From 8a8a6ae67e372e3176a48b84e2b82e6840cfbda0 Mon Sep 17 00:00:00 2001 From: 3alpha <15694175+3alpha@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:45:57 +0200 Subject: [PATCH 1/2] chore: update dnscrypt-proxy and refresh build workflows --- .gitattributes | 2 + .github/workflows/main.yml | 10 +- .github/workflows/release-on-demanda.yml | 6 +- bind/Dockerfile | 18 +- bind/custom/patches/dappnode-forwarding.patch | 178 ++++ bind/custom/src/common.go | 175 ---- bind/custom/src/config.go | 958 ------------------ bind/custom/src/dappnode_forwarding_test.go | 144 +++ bind/custom/src/plugin_forward.go | 151 --- bind/custom/src/proxy.go | 880 ---------------- dappnode_package.json | 2 +- docker-compose.yml | 2 +- 12 files changed, 346 insertions(+), 2180 deletions(-) create mode 100644 .gitattributes create mode 100644 bind/custom/patches/dappnode-forwarding.patch delete mode 100644 bind/custom/src/common.go delete mode 100644 bind/custom/src/config.go create mode 100644 bind/custom/src/dappnode_forwarding_test.go delete mode 100644 bind/custom/src/plugin_forward.go delete mode 100644 bind/custom/src/proxy.go diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..467060d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Unified patch context prefixes are followed by upstream Go indentation. +bind/custom/patches/*.patch -whitespace diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dc180d3..ed96dbd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,19 +16,19 @@ jobs: if: github.event_name != 'push' steps: - name: Checkout - uses: actions/checkout@v4 - - run: docker compose build + uses: actions/checkout@v7.0.1 + - run: docker compose build --pull --no-cache release: name: Release runs-on: ubuntu-latest if: github.event_name == 'push' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7.0.1 - name: Setup node - uses: actions/setup-node@v4 + uses: actions/setup-node@v7.1.0 with: - node-version: "18.x" + node-version: "26.12.1" - name: Publish run: npx @dappnode/dappnodesdk publish patch --dappnode_team_preset env: diff --git a/.github/workflows/release-on-demanda.yml b/.github/workflows/release-on-demanda.yml index ba83a65..f2d0dee 100644 --- a/.github/workflows/release-on-demanda.yml +++ b/.github/workflows/release-on-demanda.yml @@ -8,11 +8,11 @@ jobs: runs-on: ipfs-dev-gateway if: github.event_name == 'workflow_dispatch' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7.0.1 - name: Setup node - uses: actions/setup-node@v6 + uses: actions/setup-node@v7.1.0 with: - node-version: "20.x" + node-version: "26.12.1" - name: Log in to GitHub Container Registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - name: Publish diff --git a/bind/Dockerfile b/bind/Dockerfile index 5162900..e44d248 100644 --- a/bind/Dockerfile +++ b/bind/Dockerfile @@ -6,17 +6,23 @@ WORKDIR /app/dnscrypt-proxy ENV SRC_DIR=/app/dnscrypt-proxy/dnscrypt-proxy -# Clone dnscrypt-proxy repo and checkout to the specified version # Refresh packages and the certificate bundle before fetching source. RUN apk upgrade --no-cache && \ apk add --no-cache git 'ca-certificates>=20261020-r0' && \ - git clone https://github.com/DNSCrypt/dnscrypt-proxy /app/dnscrypt-proxy && \ - git checkout ${UPSTREAM_VERSION} + git clone --depth 1 --branch "${UPSTREAM_VERSION}" \ + https://github.com/DNSCrypt/dnscrypt-proxy /app/dnscrypt-proxy -RUN cp -R go.mod go.sum vendor +# Apply only the Dappnode changes, retaining upstream fixes in all other code. +COPY custom/patches/dappnode-forwarding.patch /tmp/dappnode-forwarding.patch +RUN git apply --check /tmp/dappnode-forwarding.patch && \ + git apply /tmp/dappnode-forwarding.patch -# Replace custom src files -COPY custom/src/common.go custom/src/config.go custom/src/proxy.go custom/src/plugin_forward.go ./dnscrypt-proxy/ +COPY custom/src/*_test.go ./dnscrypt-proxy/ + +# Upstream tests exercise unreadable files, so run them without root privileges. +USER nobody +RUN GOCACHE=/tmp/go-build go test -mod=vendor ./dnscrypt-proxy +USER root RUN cd ${SRC_DIR} && \ CGO_ENABLED=0 go build -mod vendor -ldflags="-s -w" -o /usr/local/bin/dnscrypt-proxy && \ diff --git a/bind/custom/patches/dappnode-forwarding.patch b/bind/custom/patches/dappnode-forwarding.patch new file mode 100644 index 0000000..a3c0e18 --- /dev/null +++ b/bind/custom/patches/dappnode-forwarding.patch @@ -0,0 +1,178 @@ +diff --git a/dnscrypt-proxy/config.go b/dnscrypt-proxy/config.go +index 6e6e81c..0631671 100644 +--- a/dnscrypt-proxy/config.go ++++ b/dnscrypt-proxy/config.go +@@ -59,6 +59,7 @@ type Config struct { + CacheMaxTTL uint32 `toml:"cache_max_ttl"` + RejectTTL uint32 `toml:"reject_ttl"` + CloakTTL uint32 `toml:"cloak_ttl"` ++ ForwardFallbackTTL uint32 `toml:"forward_fallback_ttl"` + QueryLog QueryLogConfig `toml:"query_log"` + NxLog NxLogConfig `toml:"nx_log"` + BlockName BlockNameConfig `toml:"blocked_names"` +@@ -144,6 +145,7 @@ func newConfig() Config { + CacheMaxTTL: 86400, + RejectTTL: 600, + CloakTTL: 600, ++ ForwardFallbackTTL: 600, + SourceRequireNoLog: true, + SourceRequireNoFilter: true, + SourceIPv4: true, +diff --git a/dnscrypt-proxy/config_loader.go b/dnscrypt-proxy/config_loader.go +index 9e609a6..ed8e6b1 100644 +--- a/dnscrypt-proxy/config_loader.go ++++ b/dnscrypt-proxy/config_loader.go +@@ -270,6 +270,7 @@ func configurePlugins(proxy *Proxy, config *Config) { + proxy.cacheMaxTTL = config.CacheMaxTTL + proxy.rejectTTL = config.RejectTTL + proxy.cloakTTL = config.CloakTTL ++ proxy.forwardFallbackTTL = config.ForwardFallbackTTL + proxy.cloakedPTR = config.CloakedPTR + + // Configure query meta +diff --git a/dnscrypt-proxy/plugin_forward.go b/dnscrypt-proxy/plugin_forward.go +index 8f3bed9..679831f 100644 +--- a/dnscrypt-proxy/plugin_forward.go ++++ b/dnscrypt-proxy/plugin_forward.go +@@ -6,14 +6,17 @@ + "fmt" + "math/rand" + "net" ++ "net/netip" + "os" + "path/filepath" + "strings" + "sync" + "sync/atomic" + "time" ++ "unicode" + + "codeberg.org/miekg/dns" ++ "codeberg.org/miekg/dns/rdata" + "github.com/jedisct1/dlog" + "github.com/lifenjoiner/dhcpdns" + netproxy "golang.org/x/net/proxy" +@@ -39,10 +42,12 @@ type PluginForwardEntry struct { + domain string + sequence []SearchSequenceItem + viaProxy bool ++ fallback string + } + + type PluginForward struct { + forwardMap []PluginForwardEntry ++ fallbackTTL uint32 + bootstrapResolvers []string + dhcpdns []*dhcpdns.Detector + proxyDialer netproxy.Dialer +@@ -65,6 +70,7 @@ func (plugin *PluginForward) Description() string { + + func (plugin *PluginForward) Init(proxy *Proxy) error { + plugin.configFile = proxy.forwardFile ++ plugin.fallbackTTL = proxy.forwardFallbackTTL + dlog.Noticef("Loading the set of forwarding rules from [%s]", plugin.configFile) + + plugin.forceTCP = proxy.forceTCP +@@ -118,6 +124,15 @@ func (plugin *PluginForward) parseForwardFile(lines string) (bool, []PluginForwa + continue + } + domain, serversStr, ok := StringTwoFields(line) ++ // Dappnode rules optionally include a fallback IP address or CNAME. ++ fallback := "" ++ if pos := strings.LastIndexFunc(serversStr, unicode.IsSpace); pos >= 0 { ++ servers := strings.TrimSpace(serversStr[:pos]) ++ // Spaces after commas and after $PROXY: belong to the resolver list. ++ if !strings.HasSuffix(servers, ",") && servers != "$PROXY:" { ++ serversStr, fallback = servers, strings.TrimSpace(serversStr[pos:]) ++ } ++ } + domain = strings.TrimPrefix(domain, "*.") + if strings.Contains(domain, "*") { + ok = false +@@ -233,6 +248,7 @@ func (plugin *PluginForward) parseForwardFile(lines string) (bool, []PluginForwa + domain: domain, + sequence: sequence, + viaProxy: viaProxy, ++ fallback: fallback, + }) + } + +@@ -319,6 +335,7 @@ func (plugin *PluginForward) Eval(pluginsState *PluginsState, msg *dns.Msg) erro + // Use read lock for thread-safe access to forwardMap + plugin.rwLock.RLock() + var sequence []SearchSequenceItem ++ var fallback string + viaProxy := false + for _, candidate := range plugin.forwardMap { + candidateLen := len(candidate.domain) +@@ -329,6 +346,7 @@ func (plugin *PluginForward) Eval(pluginsState *PluginsState, msg *dns.Msg) erro + (candidateLen == qNameLen || (qName[qNameLen-candidateLen-1] == '.'))) || + (candidate.domain == ".") { + sequence = candidate.sequence ++ fallback = candidate.fallback + viaProxy = candidate.viaProxy + break + } +@@ -435,6 +453,9 @@ func (plugin *PluginForward) Eval(pluginsState *PluginsState, msg *dns.Msg) erro + if err := validateResponseQuestion(forwardMsg, respMsg); err != nil { + continue + } ++ if fallback != "" && respMsg.Rcode != dns.RcodeSuccess { ++ continue ++ } + if !respMsg.Security { + respMsg.AuthenticatedData = false + } +@@ -450,9 +471,40 @@ func (plugin *PluginForward) Eval(pluginsState *PluginsState, msg *dns.Msg) erro + } + return nil + } ++ if fallback != "" { ++ pluginsState.synthResponse = plugin.fallbackResponse(msg, fallback) ++ pluginsState.action = PluginsActionSynth ++ pluginsState.returnCode = PluginsReturnCodeForward ++ return nil ++ } + return err + } + ++// fallbackResponse preserves Dappnode's forwarding fallback when the local ++// resolver cannot answer, for example when resolving an unknown .eth name. ++func (plugin *PluginForward) fallbackResponse(msg *dns.Msg, target string) *dns.Msg { ++ response := EmptyResponseFromMessage(msg) ++ if len(msg.Question) == 0 { ++ return response ++ } ++ header := dns.Header{ ++ Name: msg.Question[0].Header().Name, Class: dns.ClassINET, TTL: plugin.fallbackTTL, ++ } ++ if ip, err := netip.ParseAddr(target); err == nil { ++ qtype := dns.RRToType(msg.Question[0]) ++ if ip.Is4() && qtype == dns.TypeA { ++ response.Answer = []dns.RR{&dns.A{Hdr: header, A: rdata.A{Addr: ip}}} ++ } else if ip.Is6() && qtype == dns.TypeAAAA { ++ response.Answer = []dns.RR{&dns.AAAA{Hdr: header, AAAA: rdata.AAAA{Addr: ip}}} ++ } ++ } else { ++ response.Answer = []dns.RR{&dns.CNAME{ ++ Hdr: header, CNAME: rdata.CNAME{Target: strings.TrimSuffix(target, ".") + "."}, ++ }} ++ } ++ return response ++} ++ + // exchangeDirect sends a query to the selected upstream server, starting with + // the given protocol and retrying a truncated UDP response over TCP. + func (plugin *PluginForward) exchangeDirect( +diff --git a/dnscrypt-proxy/proxy.go b/dnscrypt-proxy/proxy.go +index 726886d..b74789e 100644 +--- a/dnscrypt-proxy/proxy.go ++++ b/dnscrypt-proxy/proxy.go +@@ -91,6 +91,7 @@ type Proxy struct { + cacheMinTTL uint32 + cacheNegMaxTTL uint32 + cloakTTL uint32 ++ forwardFallbackTTL uint32 + cloakedPTR bool + cache bool + pluginBlockIPv6 bool diff --git a/bind/custom/src/common.go b/bind/custom/src/common.go deleted file mode 100644 index 05e7491..0000000 --- a/bind/custom/src/common.go +++ /dev/null @@ -1,175 +0,0 @@ -package main - -import ( - "bytes" - "encoding/binary" - "errors" - "net" - "os" - "strconv" - "strings" - "unicode" -) - -type CryptoConstruction uint16 - -const ( - UndefinedConstruction CryptoConstruction = iota - XSalsa20Poly1305 - XChacha20Poly1305 -) - -const ( - ClientMagicLen = 8 -) - -const ( - MaxHTTPBodyLength = 1000000 -) - -var ( - CertMagic = [4]byte{0x44, 0x4e, 0x53, 0x43} - ServerMagic = [8]byte{0x72, 0x36, 0x66, 0x6e, 0x76, 0x57, 0x6a, 0x38} - MinDNSPacketSize = 12 + 5 - MaxDNSPacketSize = 4096 - MaxDNSUDPPacketSize = 4096 - MaxDNSUDPSafePacketSize = 1252 - InitialMinQuestionSize = 512 -) - -var ( - FileDescriptors = make([]*os.File, 0) - FileDescriptorNum = uintptr(0) -) - -const ( - InheritedDescriptorsBase = uintptr(50) -) - -func PrefixWithSize(packet []byte) ([]byte, error) { - packetLen := len(packet) - if packetLen > 0xffff { - return packet, errors.New("Packet too large") - } - packet = append(append(packet, 0), 0) - copy(packet[2:], packet[:len(packet)-2]) - binary.BigEndian.PutUint16(packet[0:2], uint16(len(packet)-2)) - return packet, nil -} - -func ReadPrefixed(conn *net.Conn) ([]byte, error) { - buf := make([]byte, 2+MaxDNSPacketSize) - packetLength, pos := -1, 0 - for { - readnb, err := (*conn).Read(buf[pos:]) - if err != nil { - return buf, err - } - pos += readnb - if pos >= 2 && packetLength < 0 { - packetLength = int(binary.BigEndian.Uint16(buf[0:2])) - if packetLength > MaxDNSPacketSize-1 { - return buf, errors.New("Packet too large") - } - if packetLength < MinDNSPacketSize { - return buf, errors.New("Packet too short") - } - } - if packetLength >= 0 && pos >= 2+packetLength { - return buf[2 : 2+packetLength], nil - } - } -} - -func Min(a, b int) int { - if a < b { - return a - } - return b -} - -func Max(a, b int) int { - if a > b { - return a - } - return b -} - -func StringReverse(s string) string { - r := []rune(s) - for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 { - r[i], r[j] = r[j], r[i] - } - return string(r) -} - -func StringTwoFields(str string) (string, string, bool) { - if len(str) < 3 { - return "", "", false - } - pos := strings.IndexFunc(str, unicode.IsSpace) - if pos == -1 { - return "", "", false - } - a, b := strings.TrimSpace(str[:pos]), strings.TrimSpace(str[pos+1:]) - if len(a) == 0 || len(b) == 0 { - return a, b, false - } - return a, b, true -} - -func StringThreeFields(str string) (string, string, string, bool) { - - a, remainder, ok := StringTwoFields(str) - if !ok { - return "", "", "", false - } - - b, c, ok := StringTwoFields(remainder) - return a, b, c, ok -} - -func StringQuote(str string) string { - str = strconv.QuoteToGraphic(str) - return str[1 : len(str)-1] -} - -func StringStripSpaces(str string) string { - return strings.Map(func(r rune) rune { - if unicode.IsSpace(r) { - return -1 - } - return r - }, str) -} - -func TrimAndStripInlineComments(str string) string { - if idx := strings.LastIndexByte(str, '#'); idx >= 0 { - if idx == 0 || str[0] == '#' { - return "" - } - if prev := str[idx-1]; prev == ' ' || prev == '\t' { - str = str[:idx-1] - } - } - return strings.TrimSpace(str) -} - -func ExtractHostAndPort(str string, defaultPort int) (host string, port int) { - host, port = str, defaultPort - if idx := strings.LastIndex(str, ":"); idx >= 0 && idx < len(str)-1 { - if portX, err := strconv.Atoi(str[idx+1:]); err == nil { - host, port = host[:idx], portX - } - } - return -} - -func ReadTextFile(filename string) (string, error) { - bin, err := os.ReadFile(filename) - if err != nil { - return "", err - } - bin = bytes.TrimPrefix(bin, []byte{0xef, 0xbb, 0xbf}) - return string(bin), nil -} diff --git a/bind/custom/src/config.go b/bind/custom/src/config.go deleted file mode 100644 index 8e0e4a9..0000000 --- a/bind/custom/src/config.go +++ /dev/null @@ -1,958 +0,0 @@ -package main - -import ( - "encoding/json" - "errors" - "flag" - "fmt" - "math/rand" - "net" - "net/http" - "net/url" - "os" - "path" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/BurntSushi/toml" - "github.com/jedisct1/dlog" - stamps "github.com/jedisct1/go-dnsstamps" - netproxy "golang.org/x/net/proxy" -) - -const ( - MaxTimeout = 3600 - DefaultNetprobeAddress = "9.9.9.9:53" -) - -type Config struct { - LogLevel int `toml:"log_level"` - LogFile *string `toml:"log_file"` - LogFileLatest bool `toml:"log_file_latest"` - UseSyslog bool `toml:"use_syslog"` - ServerNames []string `toml:"server_names"` - DisabledServerNames []string `toml:"disabled_server_names"` - ListenAddresses []string `toml:"listen_addresses"` - LocalDoH LocalDoHConfig `toml:"local_doh"` - UserName string `toml:"user_name"` - ForceTCP bool `toml:"force_tcp"` - HTTP3 bool `toml:"http3"` - Timeout int `toml:"timeout"` - KeepAlive int `toml:"keepalive"` - Proxy string `toml:"proxy"` - CertRefreshDelay int `toml:"cert_refresh_delay"` - CertIgnoreTimestamp bool `toml:"cert_ignore_timestamp"` - EphemeralKeys bool `toml:"dnscrypt_ephemeral_keys"` - LBStrategy string `toml:"lb_strategy"` - LBEstimator bool `toml:"lb_estimator"` - BlockIPv6 bool `toml:"block_ipv6"` - BlockUnqualified bool `toml:"block_unqualified"` - BlockUndelegated bool `toml:"block_undelegated"` - Cache bool - CacheSize int `toml:"cache_size"` - CacheNegTTL uint32 `toml:"cache_neg_ttl"` - CacheNegMinTTL uint32 `toml:"cache_neg_min_ttl"` - CacheNegMaxTTL uint32 `toml:"cache_neg_max_ttl"` - CacheMinTTL uint32 `toml:"cache_min_ttl"` - CacheMaxTTL uint32 `toml:"cache_max_ttl"` - RejectTTL uint32 `toml:"reject_ttl"` - CloakTTL uint32 `toml:"cloak_ttl"` - ForwardFallbackTTL uint32 `toml:"forward_fallback_ttl"` - QueryLog QueryLogConfig `toml:"query_log"` - NxLog NxLogConfig `toml:"nx_log"` - BlockName BlockNameConfig `toml:"blocked_names"` - BlockNameLegacy BlockNameConfigLegacy `toml:"blacklist"` - WhitelistNameLegacy WhitelistNameConfigLegacy `toml:"whitelist"` - AllowedName AllowedNameConfig `toml:"allowed_names"` - BlockIP BlockIPConfig `toml:"blocked_ips"` - BlockIPLegacy BlockIPConfigLegacy `toml:"ip_blacklist"` - AllowIP AllowIPConfig `toml:"allowed_ips"` - ForwardFile string `toml:"forwarding_rules"` - CloakFile string `toml:"cloaking_rules"` - CaptivePortals CaptivePortalsConfig `toml:"captive_portals"` - StaticsConfig map[string]StaticConfig `toml:"static"` - SourcesConfig map[string]SourceConfig `toml:"sources"` - BrokenImplementations BrokenImplementationsConfig `toml:"broken_implementations"` - SourceRequireDNSSEC bool `toml:"require_dnssec"` - SourceRequireNoLog bool `toml:"require_nolog"` - SourceRequireNoFilter bool `toml:"require_nofilter"` - SourceDNSCrypt bool `toml:"dnscrypt_servers"` - SourceDoH bool `toml:"doh_servers"` - SourceODoH bool `toml:"odoh_servers"` - SourceIPv4 bool `toml:"ipv4_servers"` - SourceIPv6 bool `toml:"ipv6_servers"` - MaxClients uint32 `toml:"max_clients"` - BootstrapResolversLegacy []string `toml:"fallback_resolvers"` - BootstrapResolvers []string `toml:"bootstrap_resolvers"` - IgnoreSystemDNS bool `toml:"ignore_system_dns"` - AllWeeklyRanges map[string]WeeklyRangesStr `toml:"schedules"` - LogMaxSize int `toml:"log_files_max_size"` - LogMaxAge int `toml:"log_files_max_age"` - LogMaxBackups int `toml:"log_files_max_backups"` - TLSDisableSessionTickets bool `toml:"tls_disable_session_tickets"` - TLSCipherSuite []uint16 `toml:"tls_cipher_suite"` - TLSKeyLogFile string `toml:"tls_key_log_file"` - NetprobeAddress string `toml:"netprobe_address"` - NetprobeTimeout int `toml:"netprobe_timeout"` - OfflineMode bool `toml:"offline_mode"` - HTTPProxyURL string `toml:"http_proxy"` - RefusedCodeInResponses bool `toml:"refused_code_in_responses"` - BlockedQueryResponse string `toml:"blocked_query_response"` - QueryMeta []string `toml:"query_meta"` - CloakedPTR bool `toml:"cloak_ptr"` - AnonymizedDNS AnonymizedDNSConfig `toml:"anonymized_dns"` - DoHClientX509Auth DoHClientX509AuthConfig `toml:"doh_client_x509_auth"` - DoHClientX509AuthLegacy DoHClientX509AuthConfig `toml:"tls_client_auth"` - DNS64 DNS64Config `toml:"dns64"` - EDNSClientSubnet []string `toml:"edns_client_subnet"` -} - -func newConfig() Config { - return Config{ - LogLevel: int(dlog.LogLevel()), - LogFileLatest: true, - ListenAddresses: []string{"127.0.0.1:53"}, - LocalDoH: LocalDoHConfig{Path: "/dns-query"}, - Timeout: 5000, - KeepAlive: 5, - CertRefreshDelay: 240, - HTTP3: false, - CertIgnoreTimestamp: false, - EphemeralKeys: false, - Cache: true, - CacheSize: 512, - CacheNegTTL: 0, - CacheNegMinTTL: 60, - CacheNegMaxTTL: 600, - CacheMinTTL: 60, - CacheMaxTTL: 86400, - RejectTTL: 600, - CloakTTL: 600, - ForwardFallbackTTL: 600, - SourceRequireNoLog: true, - SourceRequireNoFilter: true, - SourceIPv4: true, - SourceIPv6: false, - SourceDNSCrypt: true, - SourceDoH: true, - SourceODoH: false, - MaxClients: 250, - BootstrapResolvers: []string{DefaultBootstrapResolver}, - IgnoreSystemDNS: false, - LogMaxSize: 10, - LogMaxAge: 7, - LogMaxBackups: 1, - TLSDisableSessionTickets: false, - TLSCipherSuite: nil, - TLSKeyLogFile: "", - NetprobeTimeout: 60, - OfflineMode: false, - RefusedCodeInResponses: false, - LBEstimator: true, - BlockedQueryResponse: "hinfo", - BrokenImplementations: BrokenImplementationsConfig{ - FragmentsBlocked: []string{ - "cisco", "cisco-ipv6", "cisco-familyshield", "cisco-familyshield-ipv6", - "cleanbrowsing-adult", "cleanbrowsing-adult-ipv6", "cleanbrowsing-family", "cleanbrowsing-family-ipv6", "cleanbrowsing-security", "cleanbrowsing-security-ipv6", - }, - }, - AnonymizedDNS: AnonymizedDNSConfig{ - DirectCertFallback: true, - }, - CloakedPTR: false, - } -} - -type StaticConfig struct { - Stamp string -} - -type SourceConfig struct { - URL string - URLs []string - MinisignKeyStr string `toml:"minisign_key"` - CacheFile string `toml:"cache_file"` - FormatStr string `toml:"format"` - RefreshDelay int `toml:"refresh_delay"` - Prefix string -} - -type QueryLogConfig struct { - File string - Format string - IgnoredQtypes []string `toml:"ignored_qtypes"` -} - -type NxLogConfig struct { - File string - Format string -} - -type BlockNameConfig struct { - File string `toml:"blocked_names_file"` - LogFile string `toml:"log_file"` - Format string `toml:"log_format"` -} - -type BlockNameConfigLegacy struct { - File string `toml:"blacklist_file"` - LogFile string `toml:"log_file"` - Format string `toml:"log_format"` -} - -type WhitelistNameConfigLegacy struct { - File string `toml:"whitelist_file"` - LogFile string `toml:"log_file"` - Format string `toml:"log_format"` -} - -type AllowedNameConfig struct { - File string `toml:"allowed_names_file"` - LogFile string `toml:"log_file"` - Format string `toml:"log_format"` -} - -type BlockIPConfig struct { - File string `toml:"blocked_ips_file"` - LogFile string `toml:"log_file"` - Format string `toml:"log_format"` -} - -type BlockIPConfigLegacy struct { - File string `toml:"blacklist_file"` - LogFile string `toml:"log_file"` - Format string `toml:"log_format"` -} - -type AllowIPConfig struct { - File string `toml:"allowed_ips_file"` - LogFile string `toml:"log_file"` - Format string `toml:"log_format"` -} - -type AnonymizedDNSRouteConfig struct { - ServerName string `toml:"server_name"` - RelayNames []string `toml:"via"` -} - -type AnonymizedDNSConfig struct { - Routes []AnonymizedDNSRouteConfig `toml:"routes"` - SkipIncompatible bool `toml:"skip_incompatible"` - DirectCertFallback bool `toml:"direct_cert_fallback"` -} - -type BrokenImplementationsConfig struct { - BrokenQueryPadding []string `toml:"broken_query_padding"` - FragmentsBlocked []string `toml:"fragments_blocked"` -} - -type LocalDoHConfig struct { - ListenAddresses []string `toml:"listen_addresses"` - Path string `toml:"path"` - CertFile string `toml:"cert_file"` - CertKeyFile string `toml:"cert_key_file"` -} - -type ServerSummary struct { - Name string `json:"name"` - Proto string `json:"proto"` - IPv6 bool `json:"ipv6"` - Addrs []string `json:"addrs,omitempty"` - Ports []int `json:"ports"` - DNSSEC bool `json:"dnssec"` - NoLog bool `json:"nolog"` - NoFilter bool `json:"nofilter"` - Description string `json:"description,omitempty"` - Stamp string `json:"stamp"` -} - -type TLSClientAuthCredsConfig struct { - ServerName string `toml:"server_name"` - ClientCert string `toml:"client_cert"` - ClientKey string `toml:"client_key"` - RootCA string `toml:"root_ca"` -} - -type DoHClientX509AuthConfig struct { - Creds []TLSClientAuthCredsConfig `toml:"creds"` -} - -type DNS64Config struct { - Prefixes []string `toml:"prefix"` - Resolvers []string `toml:"resolver"` -} - -type CaptivePortalsConfig struct { - MapFile string `toml:"map_file"` -} - -type ConfigFlags struct { - Resolve *string - List *bool - ListAll *bool - JSONOutput *bool - Check *bool - ConfigFile *string - Child *bool - NetprobeTimeoutOverride *int - ShowCerts *bool -} - -func findConfigFile(configFile *string) (string, error) { - if _, err := os.Stat(*configFile); os.IsNotExist(err) { - cdLocal() - if _, err := os.Stat(*configFile); err != nil { - return "", err - } - } - pwd, err := os.Getwd() - if err != nil { - return "", err - } - if filepath.IsAbs(*configFile) { - return *configFile, nil - } - return path.Join(pwd, *configFile), nil -} - -func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error { - foundConfigFile, err := findConfigFile(flags.ConfigFile) - if err != nil { - return fmt.Errorf( - "Unable to load the configuration file [%s] -- Maybe use the -config command-line switch?", - *flags.ConfigFile, - ) - } - config := newConfig() - md, err := toml.DecodeFile(foundConfigFile, &config) - if err != nil { - return err - } - - if flags.Resolve != nil && len(*flags.Resolve) > 0 { - addr := "127.0.0.1:53" - if len(config.ListenAddresses) > 0 { - addr = config.ListenAddresses[0] - } - Resolve(addr, *flags.Resolve, len(config.ServerNames) == 1) - os.Exit(0) - } - - if err := cdFileDir(foundConfigFile); err != nil { - return err - } - if config.LogLevel >= 0 && config.LogLevel < int(dlog.SeverityLast) { - dlog.SetLogLevel(dlog.Severity(config.LogLevel)) - } - if dlog.LogLevel() <= dlog.SeverityDebug && os.Getenv("DEBUG") == "" { - dlog.SetLogLevel(dlog.SeverityInfo) - } - dlog.TruncateLogFile(config.LogFileLatest) - proxy.showCerts = *flags.ShowCerts || len(os.Getenv("SHOW_CERTS")) > 0 - isCommandMode := *flags.Check || proxy.showCerts || *flags.List || *flags.ListAll - if isCommandMode { - } else if config.UseSyslog { - dlog.UseSyslog(true) - } else if config.LogFile != nil { - dlog.UseLogFile(*config.LogFile) - if !*flags.Child { - FileDescriptors = append(FileDescriptors, dlog.GetFileDescriptor()) - } else { - dlog.SetFileDescriptor(os.NewFile(uintptr(InheritedDescriptorsBase+FileDescriptorNum), "logFile")) - FileDescriptorNum++ - } - } - if !*flags.Child { - dlog.Noticef("dnscrypt-proxy %s", AppVersion) - } - undecoded := md.Undecoded() - if len(undecoded) > 0 { - return fmt.Errorf("Unsupported key in configuration file: [%s]", undecoded[0]) - } - - proxy.logMaxSize = config.LogMaxSize - proxy.logMaxAge = config.LogMaxAge - proxy.logMaxBackups = config.LogMaxBackups - - proxy.userName = config.UserName - - proxy.child = *flags.Child - proxy.xTransport = NewXTransport() - proxy.xTransport.tlsDisableSessionTickets = config.TLSDisableSessionTickets - proxy.xTransport.tlsCipherSuite = config.TLSCipherSuite - proxy.xTransport.mainProto = proxy.mainProto - proxy.xTransport.http3 = config.HTTP3 - if len(config.BootstrapResolvers) == 0 && len(config.BootstrapResolversLegacy) > 0 { - dlog.Warnf("fallback_resolvers was renamed to bootstrap_resolvers - Please update your configuration") - config.BootstrapResolvers = config.BootstrapResolversLegacy - } - if len(config.BootstrapResolvers) > 0 { - for _, resolver := range config.BootstrapResolvers { - if err := isIPAndPort(resolver); err != nil { - return fmt.Errorf("Bootstrap resolver [%v]: %v", resolver, err) - } - } - proxy.xTransport.ignoreSystemDNS = config.IgnoreSystemDNS - } - proxy.xTransport.bootstrapResolvers = config.BootstrapResolvers - proxy.xTransport.useIPv4 = config.SourceIPv4 - proxy.xTransport.useIPv6 = config.SourceIPv6 - proxy.xTransport.keepAlive = time.Duration(config.KeepAlive) * time.Second - if len(config.HTTPProxyURL) > 0 { - httpProxyURL, err := url.Parse(config.HTTPProxyURL) - if err != nil { - return fmt.Errorf("Unable to parse the HTTP proxy URL [%v]", config.HTTPProxyURL) - } - proxy.xTransport.httpProxyFunction = http.ProxyURL(httpProxyURL) - } - - if len(config.Proxy) > 0 { - proxyDialerURL, err := url.Parse(config.Proxy) - if err != nil { - return fmt.Errorf("Unable to parse the proxy URL [%v]", config.Proxy) - } - proxyDialer, err := netproxy.FromURL(proxyDialerURL, netproxy.Direct) - if err != nil { - return fmt.Errorf("Unable to use the proxy: [%v]", err) - } - proxy.xTransport.proxyDialer = &proxyDialer - proxy.mainProto = "tcp" - } - - proxy.xTransport.rebuildTransport() - - if md.IsDefined("refused_code_in_responses") { - dlog.Notice("config option `refused_code_in_responses` is deprecated, use `blocked_query_response`") - if config.RefusedCodeInResponses { - config.BlockedQueryResponse = "refused" - } else { - config.BlockedQueryResponse = "hinfo" - } - } - proxy.blockedQueryResponse = config.BlockedQueryResponse - proxy.timeout = time.Duration(config.Timeout) * time.Millisecond - proxy.maxClients = config.MaxClients - proxy.mainProto = "udp" - if config.ForceTCP { - proxy.mainProto = "tcp" - } - proxy.certRefreshDelay = time.Duration(Max(60, config.CertRefreshDelay)) * time.Minute - proxy.certRefreshDelayAfterFailure = time.Duration(10 * time.Second) - proxy.certIgnoreTimestamp = config.CertIgnoreTimestamp - proxy.ephemeralKeys = config.EphemeralKeys - if len(config.ListenAddresses) == 0 && len(config.LocalDoH.ListenAddresses) == 0 { - dlog.Debug("No local IP/port configured") - } - lbStrategy := LBStrategy(DefaultLBStrategy) - switch lbStrategyStr := strings.ToLower(config.LBStrategy); lbStrategyStr { - case "": - // default - case "p2": - lbStrategy = LBStrategyP2{} - case "ph": - lbStrategy = LBStrategyPH{} - case "fastest": - case "first": - lbStrategy = LBStrategyFirst{} - case "random": - lbStrategy = LBStrategyRandom{} - default: - if strings.HasPrefix(lbStrategyStr, "p") { - n, err := strconv.ParseInt(strings.TrimPrefix(lbStrategyStr, "p"), 10, 32) - if err != nil || n <= 0 { - dlog.Warnf("Invalid load balancing strategy: [%s]", config.LBStrategy) - } else { - lbStrategy = LBStrategyPN{n: int(n)} - } - } else { - dlog.Warnf("Unknown load balancing strategy: [%s]", config.LBStrategy) - } - } - proxy.serversInfo.lbStrategy = lbStrategy - proxy.serversInfo.lbEstimator = config.LBEstimator - - proxy.listenAddresses = config.ListenAddresses - proxy.localDoHListenAddresses = config.LocalDoH.ListenAddresses - if len(config.LocalDoH.Path) > 0 && config.LocalDoH.Path[0] != '/' { - return fmt.Errorf("local DoH: [%s] cannot be a valid URL path. Read the documentation", config.LocalDoH.Path) - } - proxy.localDoHPath = config.LocalDoH.Path - proxy.localDoHCertFile = config.LocalDoH.CertFile - proxy.localDoHCertKeyFile = config.LocalDoH.CertKeyFile - proxy.pluginBlockIPv6 = config.BlockIPv6 - proxy.pluginBlockUnqualified = config.BlockUnqualified - proxy.pluginBlockUndelegated = config.BlockUndelegated - proxy.cache = config.Cache - proxy.cacheSize = config.CacheSize - - if config.CacheNegTTL > 0 { - proxy.cacheNegMinTTL = config.CacheNegTTL - proxy.cacheNegMaxTTL = config.CacheNegTTL - } else { - proxy.cacheNegMinTTL = config.CacheNegMinTTL - proxy.cacheNegMaxTTL = config.CacheNegMaxTTL - } - - proxy.cacheMinTTL = config.CacheMinTTL - proxy.cacheMaxTTL = config.CacheMaxTTL - proxy.rejectTTL = config.RejectTTL - proxy.cloakTTL = config.CloakTTL - proxy.ForwardFallbackTTL = config.ForwardFallbackTTL - proxy.cloakedPTR = config.CloakedPTR - - proxy.queryMeta = config.QueryMeta - - if len(config.EDNSClientSubnet) != 0 { - proxy.ednsClientSubnets = make([]*net.IPNet, 0) - for _, cidr := range config.EDNSClientSubnet { - _, net, err := net.ParseCIDR(cidr) - if err != nil { - return fmt.Errorf("Invalid EDNS-client-subnet CIDR: [%v]", cidr) - } - proxy.ednsClientSubnets = append(proxy.ednsClientSubnets, net) - } - } - - if len(config.QueryLog.Format) == 0 { - config.QueryLog.Format = "tsv" - } else { - config.QueryLog.Format = strings.ToLower(config.QueryLog.Format) - } - if config.QueryLog.Format != "tsv" && config.QueryLog.Format != "ltsv" { - return errors.New("Unsupported query log format") - } - proxy.queryLogFile = config.QueryLog.File - proxy.queryLogFormat = config.QueryLog.Format - proxy.queryLogIgnoredQtypes = config.QueryLog.IgnoredQtypes - - if len(config.NxLog.Format) == 0 { - config.NxLog.Format = "tsv" - } else { - config.NxLog.Format = strings.ToLower(config.NxLog.Format) - } - if config.NxLog.Format != "tsv" && config.NxLog.Format != "ltsv" { - return errors.New("Unsupported NX log format") - } - proxy.nxLogFile = config.NxLog.File - proxy.nxLogFormat = config.NxLog.Format - - if len(config.BlockName.File) > 0 && len(config.BlockNameLegacy.File) > 0 { - return errors.New("Don't specify both [blocked_names] and [blacklist] sections - Update your config file") - } - if len(config.BlockNameLegacy.File) > 0 { - dlog.Notice("Use of [blacklist] is deprecated - Update your config file") - config.BlockName.File = config.BlockNameLegacy.File - config.BlockName.Format = config.BlockNameLegacy.Format - config.BlockName.LogFile = config.BlockNameLegacy.LogFile - } - if len(config.BlockName.Format) == 0 { - config.BlockName.Format = "tsv" - } else { - config.BlockName.Format = strings.ToLower(config.BlockName.Format) - } - if config.BlockName.Format != "tsv" && config.BlockName.Format != "ltsv" { - return errors.New("Unsupported block log format") - } - proxy.blockNameFile = config.BlockName.File - proxy.blockNameFormat = config.BlockName.Format - proxy.blockNameLogFile = config.BlockName.LogFile - - if len(config.AllowedName.File) > 0 && len(config.WhitelistNameLegacy.File) > 0 { - return errors.New("Don't specify both [whitelist] and [allowed_names] sections - Update your config file") - } - if len(config.WhitelistNameLegacy.File) > 0 { - dlog.Notice("Use of [whitelist] is deprecated - Update your config file") - config.AllowedName.File = config.WhitelistNameLegacy.File - config.AllowedName.Format = config.WhitelistNameLegacy.Format - config.AllowedName.LogFile = config.WhitelistNameLegacy.LogFile - } - if len(config.AllowedName.Format) == 0 { - config.AllowedName.Format = "tsv" - } else { - config.AllowedName.Format = strings.ToLower(config.AllowedName.Format) - } - if config.AllowedName.Format != "tsv" && config.AllowedName.Format != "ltsv" { - return errors.New("Unsupported allowed_names log format") - } - proxy.allowNameFile = config.AllowedName.File - proxy.allowNameFormat = config.AllowedName.Format - proxy.allowNameLogFile = config.AllowedName.LogFile - - if len(config.BlockIP.File) > 0 && len(config.BlockIPLegacy.File) > 0 { - return errors.New("Don't specify both [blocked_ips] and [ip_blacklist] sections - Update your config file") - } - if len(config.BlockIPLegacy.File) > 0 { - dlog.Notice("Use of [ip_blacklist] is deprecated - Update your config file") - config.BlockIP.File = config.BlockIPLegacy.File - config.BlockIP.Format = config.BlockIPLegacy.Format - config.BlockIP.LogFile = config.BlockIPLegacy.LogFile - } - if len(config.BlockIP.Format) == 0 { - config.BlockIP.Format = "tsv" - } else { - config.BlockIP.Format = strings.ToLower(config.BlockIP.Format) - } - if config.BlockIP.Format != "tsv" && config.BlockIP.Format != "ltsv" { - return errors.New("Unsupported IP block log format") - } - proxy.blockIPFile = config.BlockIP.File - proxy.blockIPFormat = config.BlockIP.Format - proxy.blockIPLogFile = config.BlockIP.LogFile - - if len(config.AllowIP.Format) == 0 { - config.AllowIP.Format = "tsv" - } else { - config.AllowIP.Format = strings.ToLower(config.AllowIP.Format) - } - if config.AllowIP.Format != "tsv" && config.AllowIP.Format != "ltsv" { - return errors.New("Unsupported allowed_ips log format") - } - proxy.allowedIPFile = config.AllowIP.File - proxy.allowedIPFormat = config.AllowIP.Format - proxy.allowedIPLogFile = config.AllowIP.LogFile - - proxy.forwardFile = config.ForwardFile - proxy.cloakFile = config.CloakFile - proxy.captivePortalMapFile = config.CaptivePortals.MapFile - - allWeeklyRanges, err := ParseAllWeeklyRanges(config.AllWeeklyRanges) - if err != nil { - return err - } - proxy.allWeeklyRanges = allWeeklyRanges - - if configRoutes := config.AnonymizedDNS.Routes; configRoutes != nil { - routes := make(map[string][]string) - for _, configRoute := range configRoutes { - routes[configRoute.ServerName] = configRoute.RelayNames - } - proxy.routes = &routes - } - proxy.skipAnonIncompatibleResolvers = config.AnonymizedDNS.SkipIncompatible - proxy.anonDirectCertFallback = config.AnonymizedDNS.DirectCertFallback - - if len(config.TLSKeyLogFile) > 0 { - f, err := os.OpenFile(config.TLSKeyLogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) - if err != nil { - dlog.Fatalf("Unable to create key log file [%s]: [%s]", config.TLSKeyLogFile, err) - } - dlog.Warnf("TLS key log file [%s] enabled", config.TLSKeyLogFile) - proxy.xTransport.keyLogWriter = f - proxy.xTransport.rebuildTransport() - } - - if config.DoHClientX509AuthLegacy.Creds != nil { - return errors.New("[tls_client_auth] has been renamed to [doh_client_x509_auth] - Update your config file") - } - dohClientCreds := config.DoHClientX509Auth.Creds - if len(dohClientCreds) > 0 { - dlog.Noticef("Enabling TLS authentication") - configClientCred := dohClientCreds[0] - if len(dohClientCreds) > 1 { - dlog.Fatal("Only one tls_client_auth entry is currently supported") - } - proxy.xTransport.tlsClientCreds = DOHClientCreds{ - clientCert: configClientCred.ClientCert, - clientKey: configClientCred.ClientKey, - rootCA: configClientCred.RootCA, - } - proxy.xTransport.rebuildTransport() - } - - // Backwards compatibility - config.BrokenImplementations.FragmentsBlocked = append( - config.BrokenImplementations.FragmentsBlocked, - config.BrokenImplementations.BrokenQueryPadding...) - - proxy.serversBlockingFragments = config.BrokenImplementations.FragmentsBlocked - - proxy.dns64Prefixes = config.DNS64.Prefixes - proxy.dns64Resolvers = config.DNS64.Resolvers - - if *flags.ListAll { - config.ServerNames = nil - config.DisabledServerNames = nil - config.SourceRequireDNSSEC = false - config.SourceRequireNoFilter = false - config.SourceRequireNoLog = false - config.SourceIPv4 = true - config.SourceIPv6 = true - config.SourceDNSCrypt = true - config.SourceDoH = true - config.SourceODoH = true - } - - var requiredProps stamps.ServerInformalProperties - if config.SourceRequireDNSSEC { - requiredProps |= stamps.ServerInformalPropertyDNSSEC - } - if config.SourceRequireNoLog { - requiredProps |= stamps.ServerInformalPropertyNoLog - } - if config.SourceRequireNoFilter { - requiredProps |= stamps.ServerInformalPropertyNoFilter - } - proxy.requiredProps = requiredProps - proxy.ServerNames = config.ServerNames - proxy.DisabledServerNames = config.DisabledServerNames - proxy.SourceIPv4 = config.SourceIPv4 - proxy.SourceIPv6 = config.SourceIPv6 - proxy.SourceDNSCrypt = config.SourceDNSCrypt - proxy.SourceDoH = config.SourceDoH - proxy.SourceODoH = config.SourceODoH - - netprobeTimeout := config.NetprobeTimeout - flag.Visit(func(flag *flag.Flag) { - if flag.Name == "netprobe-timeout" && flags.NetprobeTimeoutOverride != nil { - netprobeTimeout = *flags.NetprobeTimeoutOverride - } - }) - netprobeAddress := DefaultNetprobeAddress - if len(config.NetprobeAddress) > 0 { - netprobeAddress = config.NetprobeAddress - } else if len(config.BootstrapResolvers) > 0 { - netprobeAddress = config.BootstrapResolvers[0] - } - if !isCommandMode { - if err := NetProbe(proxy, netprobeAddress, netprobeTimeout); err != nil { - return err - } - for _, listenAddrStr := range proxy.listenAddresses { - proxy.addDNSListener(listenAddrStr) - } - for _, listenAddrStr := range proxy.localDoHListenAddresses { - proxy.addLocalDoHListener(listenAddrStr) - } - if err := proxy.addSystemDListeners(); err != nil { - return err - } - } - // if 'userName' is set and we are the parent process drop privilege and exit - if len(proxy.userName) > 0 && !proxy.child { - proxy.dropPrivilege(proxy.userName, FileDescriptors) - return errors.New( - "Dropping privileges is not supporting on this operating system. Unset `user_name` in the configuration file", - ) - } - if !config.OfflineMode { - if err := config.loadSources(proxy); err != nil { - return err - } - if len(proxy.registeredServers) == 0 { - return errors.New("No servers configured") - } - } - if *flags.List || *flags.ListAll { - if err := config.printRegisteredServers(proxy, *flags.JSONOutput); err != nil { - return err - } - os.Exit(0) - } - if proxy.routes != nil && len(*proxy.routes) > 0 { - hasSpecificRoutes := false - for _, server := range proxy.registeredServers { - if via, ok := (*proxy.routes)[server.name]; ok { - if server.stamp.Proto != stamps.StampProtoTypeDNSCrypt && - server.stamp.Proto != stamps.StampProtoTypeODoHTarget { - dlog.Errorf( - "DNS anonymization is only supported with the DNSCrypt and ODoH protocols - Connections to [%v] cannot be anonymized", - server.name, - ) - } else { - dlog.Noticef("Anonymized DNS: routing [%v] via %v", server.name, via) - } - hasSpecificRoutes = true - } - } - if via, ok := (*proxy.routes)["*"]; ok { - if hasSpecificRoutes { - dlog.Noticef("Anonymized DNS: routing everything else via %v", via) - } else { - dlog.Noticef("Anonymized DNS: routing everything via %v", via) - } - } - } - if *flags.Check { - dlog.Notice("Configuration successfully checked") - os.Exit(0) - } - return nil -} - -func (config *Config) printRegisteredServers(proxy *Proxy, jsonOutput bool) error { - var summary []ServerSummary - for _, registeredServer := range proxy.registeredServers { - addrStr, port := registeredServer.stamp.ServerAddrStr, stamps.DefaultPort - var hostAddr string - hostAddr, port = ExtractHostAndPort(addrStr, port) - addrs := make([]string, 0) - if (registeredServer.stamp.Proto == stamps.StampProtoTypeDoH || registeredServer.stamp.Proto == stamps.StampProtoTypeODoHTarget) && - len(registeredServer.stamp.ProviderName) > 0 { - providerName := registeredServer.stamp.ProviderName - var host string - host, port = ExtractHostAndPort(providerName, port) - addrs = append(addrs, host) - } - if len(addrStr) > 0 { - addrs = append(addrs, hostAddr) - } - 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, - NoLog: registeredServer.stamp.Props&stamps.ServerInformalPropertyNoLog != 0, - NoFilter: registeredServer.stamp.Props&stamps.ServerInformalPropertyNoFilter != 0, - Description: registeredServer.description, - Stamp: registeredServer.stamp.String(), - } - if jsonOutput { - summary = append(summary, serverSummary) - } else { - fmt.Println(serverSummary.Name) - } - } - if jsonOutput { - jsonStr, err := json.MarshalIndent(summary, "", " ") - if err != nil { - return err - } - fmt.Print(string(jsonStr)) - } - return nil -} - -func (config *Config) loadSources(proxy *Proxy) error { - for cfgSourceName, cfgSource_ := range config.SourcesConfig { - cfgSource := cfgSource_ - rand.Shuffle(len(cfgSource.URLs), func(i, j int) { - cfgSource.URLs[i], cfgSource.URLs[j] = cfgSource.URLs[j], cfgSource.URLs[i] - }) - if err := config.loadSource(proxy, cfgSourceName, &cfgSource); err != nil { - return err - } - } - for name, config := range config.StaticsConfig { - if stamp, err := stamps.NewServerStampFromString(config.Stamp); err == nil { - if stamp.Proto == stamps.StampProtoTypeDNSCryptRelay || stamp.Proto == stamps.StampProtoTypeODoHRelay { - dlog.Debugf("Adding [%s] to the set of available static relays", name) - registeredServer := RegisteredServer{name: name, stamp: stamp, description: "static relay"} - proxy.registeredRelays = append(proxy.registeredRelays, registeredServer) - } - } - } - if len(config.ServerNames) == 0 { - for serverName := range config.StaticsConfig { - config.ServerNames = append(config.ServerNames, serverName) - } - } - for _, serverName := range config.ServerNames { - staticConfig, ok := config.StaticsConfig[serverName] - if !ok { - continue - } - if len(staticConfig.Stamp) == 0 { - return fmt.Errorf("Missing stamp for the static [%s] definition", serverName) - } - stamp, err := stamps.NewServerStampFromString(staticConfig.Stamp) - if err != nil { - return fmt.Errorf("Stamp error for the static [%s] definition: [%v]", serverName, err) - } - proxy.registeredServers = append(proxy.registeredServers, RegisteredServer{name: serverName, stamp: stamp}) - } - if err := proxy.updateRegisteredServers(); err != nil { - return err - } - rs1 := proxy.registeredServers - rs2 := proxy.serversInfo.registeredServers - rand.Shuffle(len(rs1), func(i, j int) { - rs1[i], rs1[j] = rs1[j], rs1[i] - }) - rand.Shuffle(len(rs2), func(i, j int) { - rs2[i], rs2[j] = rs2[j], rs2[i] - }) - return nil -} - -func (config *Config) loadSource(proxy *Proxy, cfgSourceName string, cfgSource *SourceConfig) error { - if len(cfgSource.URLs) == 0 { - if len(cfgSource.URL) == 0 { - dlog.Debugf("Missing URLs for source [%s]", cfgSourceName) - } else { - cfgSource.URLs = []string{cfgSource.URL} - } - } - if cfgSource.MinisignKeyStr == "" { - return fmt.Errorf("Missing Minisign key for source [%s]", cfgSourceName) - } - if cfgSource.CacheFile == "" { - return fmt.Errorf("Missing cache file for source [%s]", cfgSourceName) - } - if cfgSource.FormatStr == "" { - cfgSource.FormatStr = "v2" - } - if cfgSource.RefreshDelay <= 0 { - cfgSource.RefreshDelay = 72 - } - cfgSource.RefreshDelay = Min(168, Max(24, cfgSource.RefreshDelay)) - source, err := NewSource( - cfgSourceName, - proxy.xTransport, - cfgSource.URLs, - cfgSource.MinisignKeyStr, - cfgSource.CacheFile, - cfgSource.FormatStr, - time.Duration(cfgSource.RefreshDelay)*time.Hour, - cfgSource.Prefix, - ) - if err != nil { - if len(source.bin) <= 0 { - dlog.Criticalf("Unable to retrieve source [%s]: [%s]", cfgSourceName, err) - return err - } - dlog.Infof("Downloading [%s] failed: %v, using cache file to startup", source.name, err) - } - proxy.sources = append(proxy.sources, source) - return nil -} - -func includesName(names []string, name string) bool { - for _, found := range names { - if strings.EqualFold(found, name) { - return true - } - } - return false -} - -func cdFileDir(fileName string) error { - return os.Chdir(filepath.Dir(fileName)) -} - -func cdLocal() { - exeFileName, err := os.Executable() - if err != nil { - dlog.Warnf( - "Unable to determine the executable directory: [%s] -- You will need to specify absolute paths in the configuration file", - err, - ) - } else if err := os.Chdir(filepath.Dir(exeFileName)); err != nil { - dlog.Warnf("Unable to change working directory to [%s]: %s", exeFileName, err) - } -} - -func isIPAndPort(addrStr string) error { - host, port := ExtractHostAndPort(addrStr, -1) - if ip := ParseIP(host); ip == nil { - return fmt.Errorf("Host does not parse as IP '%s'", addrStr) - } else if port == -1 { - return fmt.Errorf("Port missing '%s'", addrStr) - } else if _, err := strconv.ParseUint(strconv.Itoa(port), 10, 16); err != nil { - return fmt.Errorf("Port does not parse '%s' [%v]", addrStr, err) - } - return nil -} diff --git a/bind/custom/src/dappnode_forwarding_test.go b/bind/custom/src/dappnode_forwarding_test.go new file mode 100644 index 0000000..ef82807 --- /dev/null +++ b/bind/custom/src/dappnode_forwarding_test.go @@ -0,0 +1,144 @@ +package main + +import ( + "net" + "strings" + "testing" + "time" + + "codeberg.org/miekg/dns" +) + +func TestDappnodeForwardingRules(t *testing.T) { + plugin := &PluginForward{} + _, rules, err := plugin.parseForwardFile("dappnode 127.0.0.11\ndappnode.private 127.0.0.11\neth 127.0.0.11 my.dappnode\n") + if err != nil { + t.Fatal(err) + } + if len(rules) != 3 || rules[0].fallback != "" || rules[2].fallback != "my.dappnode" { + t.Fatalf("unexpected forwarding rules: %+v", rules) + } + if rules[2].sequence[0].servers[0] != "127.0.0.11:53" { + t.Fatalf("unexpected resolver: %+v", rules[2]) + } + + // The fallback extension must retain upstream resolver sequence support. + plugin.bootstrapResolvers = []string{"1.1.1.1:53"} + _, rules, err = plugin.parseForwardFile("*.eth [::1]:5353,$BOOTSTRAP my.dappnode") + if err != nil || len(rules) != 1 || len(rules[0].sequence) != 2 || rules[0].fallback != "my.dappnode" { + t.Fatalf("resolver sequence: %+v, %v", rules, err) + } + if _, _, err := plugin.parseForwardFile("bad*domain 127.0.0.11 my.dappnode"); err == nil { + t.Fatal("wildcards inside a domain must still be rejected") + } + _, rules, err = plugin.parseForwardFile("eth 127.0.0.11, 1.1.1.1") + if err != nil || len(rules) != 1 || rules[0].fallback != "" || len(rules[0].sequence[0].servers) != 2 { + t.Fatalf("spaced resolver list: %+v, %v", rules, err) + } +} + +func TestDappnodeFallbackTTL(t *testing.T) { + config := newConfig() + if config.ForwardFallbackTTL != 600 { + t.Fatalf("default fallback TTL = %d", config.ForwardFallbackTTL) + } + config.ForwardFallbackTTL = 123 + proxy := &Proxy{} + configurePlugins(proxy, &config) + if proxy.forwardFallbackTTL != 123 { + t.Fatalf("configured fallback TTL = %d", proxy.forwardFallbackTTL) + } +} + +func TestDappnodeForwardingResponses(t *testing.T) { + for _, tc := range []struct { + name string + rcode uint16 + fallback string + want string + }{ + {"successful answer", dns.RcodeSuccess, "my.dappnode", "192.0.2.1"}, + {"unknown ENS name", dns.RcodeNameError, "my.dappnode", "my.dappnode."}, + {"resolver failure", dns.RcodeServerFailure, "my.dappnode", "my.dappnode."}, + {"refused query", dns.RcodeRefused, "my.dappnode", "my.dappnode."}, + {"IP fallback", dns.RcodeNameError, "192.0.2.2", "192.0.2.2"}, + {"no fallback", dns.RcodeNameError, "", ""}, + } { + t.Run(tc.name, func(t *testing.T) { + conn, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { conn.Close() }) + go func() { + buffer := make([]byte, 4096) + n, addr, err := conn.ReadFrom(buffer) + if err != nil { + return + } + query := &dns.Msg{Data: buffer[:n]} + if query.Unpack() != nil { + return + } + response := EmptyResponseFromMessage(query) + response.Rcode = tc.rcode + if tc.rcode == dns.RcodeSuccess { + rr, err := dns.New("example.eth. 60 IN A 192.0.2.1") + if err != nil { + return + } + response.Answer = []dns.RR{rr} + } + if response.Pack() == nil { + conn.WriteTo(response.Data, addr) + } + }() + + plugin := &PluginForward{fallbackTTL: 123} + _, rules, err := plugin.parseForwardFile("eth " + conn.LocalAddr().String() + " " + tc.fallback) + if err != nil { + t.Fatal(err) + } + plugin.forwardMap = rules + query := dns.NewMsg("example.eth.", dns.TypeA) + state := &PluginsState{qName: "example.eth", timeout: time.Second} + if err := plugin.Eval(state, query); err != nil { + t.Fatal(err) + } + response := state.synthResponse + if response == nil || response.ID != query.ID || state.returnCode != PluginsReturnCodeForward { + t.Fatalf("unexpected response state: %+v", state) + } + if tc.want == "" { + if response.Rcode != tc.rcode || len(response.Answer) != 0 { + t.Fatalf("negative response changed: %v", response) + } + return + } + if response.Rcode != dns.RcodeSuccess || len(response.Answer) != 1 || !strings.HasSuffix(response.Answer[0].String(), tc.want) { + t.Fatalf("unexpected answer: %v", response) + } + if tc.rcode != dns.RcodeSuccess && (response.Answer[0].Header().TTL != 123 || response.AuthenticatedData) { + t.Fatalf("incorrect fallback TTL or DNSSEC flag: %v", response) + } + }) + } +} + +func TestDappnodeForwardingTransportFailure(t *testing.T) { + plugin := &PluginForward{ + fallbackTTL: 600, + forwardMap: []PluginForwardEntry{{ + domain: "eth", fallback: "my.dappnode", + sequence: []SearchSequenceItem{{typ: Explicit, servers: []string{"127.0.0.1:0"}}}, + }}, + } + query := dns.NewMsg("example.eth.", dns.TypeA) + state := &PluginsState{qName: "example.eth", timeout: 50 * time.Millisecond} + if err := plugin.Eval(state, query); err != nil { + t.Fatal(err) + } + if state.synthResponse == nil || len(state.synthResponse.Answer) != 1 { + t.Fatalf("missing transport failure fallback: %+v", state) + } +} diff --git a/bind/custom/src/plugin_forward.go b/bind/custom/src/plugin_forward.go deleted file mode 100644 index 0b5e505..0000000 --- a/bind/custom/src/plugin_forward.go +++ /dev/null @@ -1,151 +0,0 @@ -package main - -import ( - "fmt" - "math/rand" - "net" - "strings" - - "github.com/jedisct1/dlog" - "github.com/miekg/dns" -) - -type PluginForwardEntry struct { - domain string - servers []string - fallback string -} - -type PluginForward struct { - forwardMap []PluginForwardEntry - fallbackTTL uint32 -} - -func (plugin *PluginForward) Name() string { - return "forward" -} - -func (plugin *PluginForward) Description() string { - return "Route queries matching specific domains to a dedicated set of servers" -} - -func (plugin *PluginForward) Init(proxy *Proxy) error { - dlog.Noticef("Loading the set of forwarding rules from [%s]", proxy.forwardFile) - lines, err := ReadTextFile(proxy.forwardFile) - plugin.fallbackTTL = proxy.ForwardFallbackTTL - if err != nil { - return err - } - for lineNo, line := range strings.Split(lines, "\n") { - line = TrimAndStripInlineComments(line) - if len(line) == 0 { - continue - } - - domain, serversStr, fallback, ok := StringThreeFields(line) - if !ok { - domain, serversStr, ok = StringTwoFields(line) - if !ok { - return fmt.Errorf( - "Syntax error for a forwarding rule at line %d. Expected syntax: example.com 9.9.9.9,8.8.8.8", - 1+lineNo, - ) - } - } - domain = strings.ToLower(domain) - var servers []string - for _, server := range strings.Split(serversStr, ",") { - server = strings.TrimSpace(server) - if net.ParseIP(server) != nil { - server = fmt.Sprintf("%s:%d", server, 53) - } - servers = append(servers, server) - } - if len(servers) == 0 { - continue - } - plugin.forwardMap = append(plugin.forwardMap, PluginForwardEntry{ - domain: domain, - servers: servers, - fallback: fallback, - }) - } - return nil -} - -func (plugin *PluginForward) Drop() error { - return nil -} - -func (plugin *PluginForward) Reload() error { - return nil -} - -func (plugin *PluginForward) Eval(pluginsState *PluginsState, msg *dns.Msg) error { - qName := pluginsState.qName - qNameLen := len(qName) - var servers []string - 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 - fallback = candidate.fallback - break - } - } - if len(servers) == 0 { - return nil - } - 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()) - - 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 { - if respMsg.Truncated { - client.Net = "tcp" - respMsg, _, err = client.Exchange(msg, server) - if err != nil { - return err - } - } - 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 -} diff --git a/bind/custom/src/proxy.go b/bind/custom/src/proxy.go deleted file mode 100644 index 69c4f59..0000000 --- a/bind/custom/src/proxy.go +++ /dev/null @@ -1,880 +0,0 @@ -package main - -import ( - "context" - crypto_rand "crypto/rand" - "encoding/binary" - "math/rand" - "net" - "os" - "runtime" - "strings" - "sync/atomic" - "time" - - "github.com/jedisct1/dlog" - clocksmith "github.com/jedisct1/go-clocksmith" - stamps "github.com/jedisct1/go-dnsstamps" - "github.com/miekg/dns" - "golang.org/x/crypto/curve25519" -) - -type Proxy struct { - pluginsGlobals PluginsGlobals - serversInfo ServersInfo - questionSizeEstimator QuestionSizeEstimator - registeredServers []RegisteredServer - dns64Resolvers []string - dns64Prefixes []string - serversBlockingFragments []string - ednsClientSubnets []*net.IPNet - queryLogIgnoredQtypes []string - localDoHListeners []*net.TCPListener - queryMeta []string - udpListeners []*net.UDPConn - sources []*Source - tcpListeners []*net.TCPListener - registeredRelays []RegisteredServer - listenAddresses []string - localDoHListenAddresses []string - xTransport *XTransport - allWeeklyRanges *map[string]WeeklyRanges - routes *map[string][]string - captivePortalMap *CaptivePortalMap - nxLogFormat string - localDoHCertFile string - localDoHCertKeyFile string - captivePortalMapFile string - localDoHPath string - mainProto string - cloakFile string - forwardFile string - blockIPFormat string - blockIPLogFile string - allowedIPFile string - allowedIPFormat string - allowedIPLogFile string - queryLogFormat string - blockIPFile string - allowNameFile string - allowNameFormat string - allowNameLogFile string - blockNameLogFile string - blockNameFormat string - blockNameFile string - queryLogFile string - blockedQueryResponse string - userName string - nxLogFile string - proxySecretKey [32]byte - proxyPublicKey [32]byte - ServerNames []string - DisabledServerNames []string - requiredProps stamps.ServerInformalProperties - certRefreshDelayAfterFailure time.Duration - timeout time.Duration - certRefreshDelay time.Duration - cacheSize int - logMaxBackups int - logMaxAge int - logMaxSize int - cacheNegMinTTL uint32 - rejectTTL uint32 - cacheMaxTTL uint32 - clientsCount uint32 - maxClients uint32 - cacheMinTTL uint32 - cacheNegMaxTTL uint32 - cloakTTL uint32 - ForwardFallbackTTL uint32 - cloakedPTR bool - cache bool - pluginBlockIPv6 bool - ephemeralKeys bool - pluginBlockUnqualified bool - showCerts bool - certIgnoreTimestamp bool - skipAnonIncompatibleResolvers bool - anonDirectCertFallback bool - pluginBlockUndelegated bool - child bool - SourceIPv4 bool - SourceIPv6 bool - SourceDNSCrypt bool - SourceDoH bool - SourceODoH bool -} - -func (proxy *Proxy) registerUDPListener(conn *net.UDPConn) { - proxy.udpListeners = append(proxy.udpListeners, conn) -} - -func (proxy *Proxy) registerTCPListener(listener *net.TCPListener) { - proxy.tcpListeners = append(proxy.tcpListeners, listener) -} - -func (proxy *Proxy) registerLocalDoHListener(listener *net.TCPListener) { - proxy.localDoHListeners = append(proxy.localDoHListeners, listener) -} - -func (proxy *Proxy) addDNSListener(listenAddrStr string) { - listenUDPAddr, err := net.ResolveUDPAddr("udp", listenAddrStr) - if err != nil { - dlog.Fatal(err) - } - listenTCPAddr, err := net.ResolveTCPAddr("tcp", listenAddrStr) - if err != nil { - dlog.Fatal(err) - } - - // if 'userName' is not set, continue as before - if len(proxy.userName) <= 0 { - if err := proxy.udpListenerFromAddr(listenUDPAddr); err != nil { - dlog.Fatal(err) - } - if err := proxy.tcpListenerFromAddr(listenTCPAddr); err != nil { - dlog.Fatal(err) - } - return - } - - // if 'userName' is set and we are the parent process - if !proxy.child { - // parent - listenerUDP, err := net.ListenUDP("udp", listenUDPAddr) - if err != nil { - dlog.Fatal(err) - } - listenerTCP, err := net.ListenTCP("tcp", listenTCPAddr) - if err != nil { - dlog.Fatal(err) - } - - fdUDP, err := listenerUDP.File() // On Windows, the File method of UDPConn is not implemented. - if err != nil { - dlog.Fatalf("Unable to switch to a different user: %v", err) - } - fdTCP, err := listenerTCP.File() // On Windows, the File method of TCPListener is not implemented. - if err != nil { - dlog.Fatalf("Unable to switch to a different user: %v", err) - } - defer listenerUDP.Close() - defer listenerTCP.Close() - FileDescriptors = append(FileDescriptors, fdUDP) - FileDescriptors = append(FileDescriptors, fdTCP) - return - } - - // child - listenerUDP, err := net.FilePacketConn(os.NewFile(InheritedDescriptorsBase+FileDescriptorNum, "listenerUDP")) - if err != nil { - dlog.Fatalf("Unable to switch to a different user: %v", err) - } - FileDescriptorNum++ - - listenerTCP, err := net.FileListener(os.NewFile(InheritedDescriptorsBase+FileDescriptorNum, "listenerTCP")) - if err != nil { - dlog.Fatalf("Unable to switch to a different user: %v", err) - } - FileDescriptorNum++ - - dlog.Noticef("Now listening to %v [UDP]", listenUDPAddr) - proxy.registerUDPListener(listenerUDP.(*net.UDPConn)) - - dlog.Noticef("Now listening to %v [TCP]", listenAddrStr) - proxy.registerTCPListener(listenerTCP.(*net.TCPListener)) -} - -func (proxy *Proxy) addLocalDoHListener(listenAddrStr string) { - listenTCPAddr, err := net.ResolveTCPAddr("tcp", listenAddrStr) - if err != nil { - dlog.Fatal(err) - } - - // if 'userName' is not set, continue as before - if len(proxy.userName) <= 0 { - if err := proxy.localDoHListenerFromAddr(listenTCPAddr); err != nil { - dlog.Fatal(err) - } - return - } - - // if 'userName' is set and we are the parent process - if !proxy.child { - // parent - listenerTCP, err := net.ListenTCP("tcp", listenTCPAddr) - if err != nil { - dlog.Fatal(err) - } - fdTCP, err := listenerTCP.File() // On Windows, the File method of TCPListener is not implemented. - if err != nil { - dlog.Fatalf("Unable to switch to a different user: %v", err) - } - defer listenerTCP.Close() - FileDescriptors = append(FileDescriptors, fdTCP) - return - } - - // child - - listenerTCP, err := net.FileListener(os.NewFile(InheritedDescriptorsBase+FileDescriptorNum, "listenerTCP")) - if err != nil { - dlog.Fatalf("Unable to switch to a different user: %v", err) - } - FileDescriptorNum++ - - proxy.registerLocalDoHListener(listenerTCP.(*net.TCPListener)) - dlog.Noticef("Now listening to https://%v%v [DoH]", listenAddrStr, proxy.localDoHPath) -} - -func (proxy *Proxy) StartProxy() { - proxy.questionSizeEstimator = NewQuestionSizeEstimator() - if _, err := crypto_rand.Read(proxy.proxySecretKey[:]); err != nil { - dlog.Fatal(err) - } - curve25519.ScalarBaseMult(&proxy.proxyPublicKey, &proxy.proxySecretKey) - proxy.startAcceptingClients() - if !proxy.child { - // Notify the service manager that dnscrypt-proxy is ready. dnscrypt-proxy manages itself in case - // servers are not immediately live/reachable. The service manager may assume it is initialized and - // functioning properly. Note that the service manager 'Ready' signal is delayed if netprobe - // cannot reach the internet during start-up. - if err := ServiceManagerReadyNotify(); err != nil { - dlog.Fatal(err) - } - } - proxy.xTransport.internalResolverReady = false - proxy.xTransport.internalResolvers = proxy.listenAddresses - liveServers, err := proxy.serversInfo.refresh(proxy) - if liveServers > 0 { - proxy.certIgnoreTimestamp = false - } - if proxy.showCerts { - os.Exit(0) - } - if liveServers > 0 { - dlog.Noticef("dnscrypt-proxy is ready - live servers: %d", liveServers) - } else if err != nil { - dlog.Error(err) - dlog.Notice("dnscrypt-proxy is waiting for at least one server to be reachable") - } - go func() { - for { - clocksmith.Sleep(PrefetchSources(proxy.xTransport, proxy.sources)) - proxy.updateRegisteredServers() - runtime.GC() - } - }() - if len(proxy.serversInfo.registeredServers) > 0 { - go func() { - for { - delay := proxy.certRefreshDelay - if liveServers == 0 { - delay = proxy.certRefreshDelayAfterFailure - } - clocksmith.Sleep(delay) - liveServers, _ = proxy.serversInfo.refresh(proxy) - if liveServers > 0 { - proxy.certIgnoreTimestamp = false - } - runtime.GC() - } - }() - } -} - -func (proxy *Proxy) updateRegisteredServers() error { - for _, source := range proxy.sources { - registeredServers, err := source.Parse() - if err != nil { - if len(registeredServers) == 0 { - dlog.Criticalf("Unable to use source [%s]: [%s]", source.name, err) - return err - } - dlog.Warnf( - "Error in source [%s]: [%s] -- Continuing with reduced server count [%d]", - source.name, - err, - len(registeredServers), - ) - } - for _, registeredServer := range registeredServers { - if registeredServer.stamp.Proto != stamps.StampProtoTypeDNSCryptRelay && - registeredServer.stamp.Proto != stamps.StampProtoTypeODoHRelay { - if len(proxy.ServerNames) > 0 { - if !includesName(proxy.ServerNames, registeredServer.name) { - continue - } - } else if registeredServer.stamp.Props&proxy.requiredProps != proxy.requiredProps { - continue - } - } - if includesName(proxy.DisabledServerNames, registeredServer.name) { - continue - } - if proxy.SourceIPv4 || proxy.SourceIPv6 { - isIPv4, isIPv6 := true, false - if registeredServer.stamp.Proto == stamps.StampProtoTypeDoH { - isIPv4, isIPv6 = true, true - } - if strings.HasPrefix(registeredServer.stamp.ServerAddrStr, "[") { - isIPv4, isIPv6 = false, true - } - if !(proxy.SourceIPv4 == isIPv4 || proxy.SourceIPv6 == isIPv6) { - continue - } - } - if registeredServer.stamp.Proto == stamps.StampProtoTypeDNSCryptRelay || - registeredServer.stamp.Proto == stamps.StampProtoTypeODoHRelay { - var found bool - for i, currentRegisteredRelay := range proxy.registeredRelays { - if currentRegisteredRelay.name == registeredServer.name { - found = true - if currentRegisteredRelay.stamp.String() != registeredServer.stamp.String() { - dlog.Infof( - "Updating stamp for [%s] was: %s now: %s", - registeredServer.name, - currentRegisteredRelay.stamp.String(), - registeredServer.stamp.String(), - ) - proxy.registeredRelays[i].stamp = registeredServer.stamp - dlog.Debugf("Total count of registered relays %v", len(proxy.registeredRelays)) - } - } - } - if !found { - dlog.Debugf("Adding [%s] to the set of available relays", registeredServer.name) - proxy.registeredRelays = append(proxy.registeredRelays, registeredServer) - } - } else { - if !((proxy.SourceDNSCrypt && registeredServer.stamp.Proto == stamps.StampProtoTypeDNSCrypt) || - (proxy.SourceDoH && registeredServer.stamp.Proto == stamps.StampProtoTypeDoH) || - (proxy.SourceODoH && registeredServer.stamp.Proto == stamps.StampProtoTypeODoHTarget)) { - continue - } - var found bool - for i, currentRegisteredServer := range proxy.registeredServers { - if currentRegisteredServer.name == registeredServer.name { - found = true - if currentRegisteredServer.stamp.String() != registeredServer.stamp.String() { - dlog.Infof("Updating stamp for [%s] was: %s now: %s", registeredServer.name, currentRegisteredServer.stamp.String(), registeredServer.stamp.String()) - proxy.registeredServers[i].stamp = registeredServer.stamp - } - } - } - if !found { - dlog.Debugf("Adding [%s] to the set of wanted resolvers", registeredServer.name) - proxy.registeredServers = append(proxy.registeredServers, registeredServer) - dlog.Debugf("Total count of registered servers %v", len(proxy.registeredServers)) - } - } - } - } - for _, registeredServer := range proxy.registeredServers { - proxy.serversInfo.registerServer(registeredServer.name, registeredServer.stamp) - } - for _, registeredRelay := range proxy.registeredRelays { - proxy.serversInfo.registerRelay(registeredRelay.name, registeredRelay.stamp) - } - return nil -} - -func (proxy *Proxy) udpListener(clientPc *net.UDPConn) { - defer clientPc.Close() - for { - buffer := make([]byte, MaxDNSPacketSize-1) - length, clientAddr, err := clientPc.ReadFrom(buffer) - if err != nil { - return - } - packet := buffer[:length] - if !proxy.clientsCountInc() { - dlog.Warnf("Too many incoming connections (max=%d)", proxy.maxClients) - proxy.processIncomingQuery( - "udp", - proxy.mainProto, - packet, - &clientAddr, - clientPc, - time.Now(), - true, - ) // respond synchronously, but only to cached/synthesized queries - continue - } - go func() { - defer proxy.clientsCountDec() - proxy.processIncomingQuery("udp", proxy.mainProto, packet, &clientAddr, clientPc, time.Now(), false) - }() - } -} - -func (proxy *Proxy) tcpListener(acceptPc *net.TCPListener) { - defer acceptPc.Close() - for { - clientPc, err := acceptPc.Accept() - if err != nil { - continue - } - if !proxy.clientsCountInc() { - dlog.Warnf("Too many incoming connections (max=%d)", proxy.maxClients) - clientPc.Close() - continue - } - go func() { - defer clientPc.Close() - defer proxy.clientsCountDec() - if err := clientPc.SetDeadline(time.Now().Add(proxy.timeout)); err != nil { - return - } - start := time.Now() - packet, err := ReadPrefixed(&clientPc) - if err != nil { - return - } - clientAddr := clientPc.RemoteAddr() - proxy.processIncomingQuery("tcp", "tcp", packet, &clientAddr, clientPc, start, false) - }() - } -} - -func (proxy *Proxy) udpListenerFromAddr(listenAddr *net.UDPAddr) error { - listenConfig, err := proxy.udpListenerConfig() - if err != nil { - return err - } - clientPc, err := listenConfig.ListenPacket(context.Background(), "udp", listenAddr.String()) - if err != nil { - return err - } - proxy.registerUDPListener(clientPc.(*net.UDPConn)) - dlog.Noticef("Now listening to %v [UDP]", listenAddr) - return nil -} - -func (proxy *Proxy) tcpListenerFromAddr(listenAddr *net.TCPAddr) error { - listenConfig, err := proxy.tcpListenerConfig() - if err != nil { - return err - } - acceptPc, err := listenConfig.Listen(context.Background(), "tcp", listenAddr.String()) - if err != nil { - return err - } - proxy.registerTCPListener(acceptPc.(*net.TCPListener)) - dlog.Noticef("Now listening to %v [TCP]", listenAddr) - return nil -} - -func (proxy *Proxy) localDoHListenerFromAddr(listenAddr *net.TCPAddr) error { - listenConfig, err := proxy.tcpListenerConfig() - if err != nil { - return err - } - acceptPc, err := listenConfig.Listen(context.Background(), "tcp", listenAddr.String()) - if err != nil { - return err - } - proxy.registerLocalDoHListener(acceptPc.(*net.TCPListener)) - dlog.Noticef("Now listening to https://%v%v [DoH]", listenAddr, proxy.localDoHPath) - return nil -} - -func (proxy *Proxy) startAcceptingClients() { - for _, clientPc := range proxy.udpListeners { - go proxy.udpListener(clientPc) - } - proxy.udpListeners = nil - for _, acceptPc := range proxy.tcpListeners { - go proxy.tcpListener(acceptPc) - } - proxy.tcpListeners = nil - for _, acceptPc := range proxy.localDoHListeners { - go proxy.localDoHListener(acceptPc) - } - proxy.localDoHListeners = nil -} - -func (proxy *Proxy) prepareForRelay(ip net.IP, port int, encryptedQuery *[]byte) { - anonymizedDNSHeader := []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00} - relayedQuery := append(anonymizedDNSHeader, ip.To16()...) - var tmp [2]byte - binary.BigEndian.PutUint16(tmp[0:2], uint16(port)) - relayedQuery = append(relayedQuery, tmp[:]...) - relayedQuery = append(relayedQuery, *encryptedQuery...) - *encryptedQuery = relayedQuery -} - -func (proxy *Proxy) exchangeWithUDPServer( - serverInfo *ServerInfo, - sharedKey *[32]byte, - encryptedQuery []byte, - clientNonce []byte, -) ([]byte, error) { - upstreamAddr := serverInfo.UDPAddr - if serverInfo.Relay != nil && serverInfo.Relay.Dnscrypt != nil { - upstreamAddr = serverInfo.Relay.Dnscrypt.RelayUDPAddr - } - var err error - var pc net.Conn - proxyDialer := proxy.xTransport.proxyDialer - if proxyDialer == nil { - pc, err = net.DialUDP("udp", nil, upstreamAddr) - } else { - pc, err = (*proxyDialer).Dial("udp", upstreamAddr.String()) - } - if err != nil { - return nil, err - } - defer pc.Close() - if err := pc.SetDeadline(time.Now().Add(serverInfo.Timeout)); err != nil { - return nil, err - } - if serverInfo.Relay != nil && serverInfo.Relay.Dnscrypt != nil { - proxy.prepareForRelay(serverInfo.UDPAddr.IP, serverInfo.UDPAddr.Port, &encryptedQuery) - } - encryptedResponse := make([]byte, MaxDNSPacketSize) - for tries := 2; tries > 0; tries-- { - if _, err := pc.Write(encryptedQuery); err != nil { - return nil, err - } - length, err := pc.Read(encryptedResponse) - if err == nil { - encryptedResponse = encryptedResponse[:length] - break - } - dlog.Debugf("[%v] Retry on timeout", serverInfo.Name) - } - return proxy.Decrypt(serverInfo, sharedKey, encryptedResponse, clientNonce) -} - -func (proxy *Proxy) exchangeWithTCPServer( - serverInfo *ServerInfo, - sharedKey *[32]byte, - encryptedQuery []byte, - clientNonce []byte, -) ([]byte, error) { - upstreamAddr := serverInfo.TCPAddr - if serverInfo.Relay != nil && serverInfo.Relay.Dnscrypt != nil { - upstreamAddr = serverInfo.Relay.Dnscrypt.RelayTCPAddr - } - var err error - var pc net.Conn - proxyDialer := proxy.xTransport.proxyDialer - if proxyDialer == nil { - pc, err = net.DialTCP("tcp", nil, upstreamAddr) - } else { - pc, err = (*proxyDialer).Dial("tcp", upstreamAddr.String()) - } - if err != nil { - return nil, err - } - defer pc.Close() - if err := pc.SetDeadline(time.Now().Add(serverInfo.Timeout)); err != nil { - return nil, err - } - if serverInfo.Relay != nil && serverInfo.Relay.Dnscrypt != nil { - proxy.prepareForRelay(serverInfo.TCPAddr.IP, serverInfo.TCPAddr.Port, &encryptedQuery) - } - encryptedQuery, err = PrefixWithSize(encryptedQuery) - if err != nil { - return nil, err - } - if _, err := pc.Write(encryptedQuery); err != nil { - return nil, err - } - encryptedResponse, err := ReadPrefixed(&pc) - if err != nil { - return nil, err - } - return proxy.Decrypt(serverInfo, sharedKey, encryptedResponse, clientNonce) -} - -func (proxy *Proxy) clientsCountInc() bool { - for { - count := atomic.LoadUint32(&proxy.clientsCount) - if count >= proxy.maxClients { - return false - } - if atomic.CompareAndSwapUint32(&proxy.clientsCount, count, count+1) { - dlog.Debugf("clients count: %d", count+1) - return true - } - } -} - -func (proxy *Proxy) clientsCountDec() { - for { - if count := atomic.LoadUint32(&proxy.clientsCount); count == 0 || - atomic.CompareAndSwapUint32(&proxy.clientsCount, count, count-1) { - break - } - } -} - -func (proxy *Proxy) processIncomingQuery( - clientProto string, - serverProto string, - query []byte, - clientAddr *net.Addr, - clientPc net.Conn, - start time.Time, - onlyCached bool, -) []byte { - var response []byte - if len(query) < MinDNSPacketSize { - return response - } - pluginsState := NewPluginsState(proxy, clientProto, clientAddr, serverProto, start) - serverName := "-" - needsEDNS0Padding := false - serverInfo := proxy.serversInfo.getOne() - if serverInfo != nil { - serverName = serverInfo.Name - needsEDNS0Padding = (serverInfo.Proto == stamps.StampProtoTypeDoH || serverInfo.Proto == stamps.StampProtoTypeTLS) - } - query, _ = pluginsState.ApplyQueryPlugins(&proxy.pluginsGlobals, query, needsEDNS0Padding) - if len(query) < MinDNSPacketSize || len(query) > MaxDNSPacketSize { - return response - } - if pluginsState.action == PluginsActionDrop { - pluginsState.returnCode = PluginsReturnCodeDrop - pluginsState.ApplyLoggingPlugins(&proxy.pluginsGlobals) - return response - } - var err error - if pluginsState.synthResponse != nil { - response, err = pluginsState.synthResponse.PackBuffer(response) - if err != nil { - pluginsState.returnCode = PluginsReturnCodeParseError - pluginsState.ApplyLoggingPlugins(&proxy.pluginsGlobals) - return response - } - } - if onlyCached { - if len(response) == 0 { - return response - } - serverInfo = nil - } - if len(response) == 0 && serverInfo != nil { - var ttl *uint32 - pluginsState.serverName = serverName - if serverInfo.Proto == stamps.StampProtoTypeDNSCrypt { - sharedKey, encryptedQuery, clientNonce, err := proxy.Encrypt(serverInfo, query, serverProto) - if err != nil && serverProto == "udp" { - dlog.Debug("Unable to pad for UDP, re-encrypting query for TCP") - serverProto = "tcp" - sharedKey, encryptedQuery, clientNonce, err = proxy.Encrypt(serverInfo, query, serverProto) - } - if err != nil { - pluginsState.returnCode = PluginsReturnCodeParseError - pluginsState.ApplyLoggingPlugins(&proxy.pluginsGlobals) - return response - } - serverInfo.noticeBegin(proxy) - if serverProto == "udp" { - response, err = proxy.exchangeWithUDPServer(serverInfo, sharedKey, encryptedQuery, clientNonce) - retryOverTCP := false - if err == nil && len(response) >= MinDNSPacketSize && response[2]&0x02 == 0x02 { - retryOverTCP = true - } else if neterr, ok := err.(net.Error); ok && neterr.Timeout() { - dlog.Debugf("[%v] Retry over TCP after UDP timeouts", serverName) - retryOverTCP = true - } - if retryOverTCP { - serverProto = "tcp" - sharedKey, encryptedQuery, clientNonce, err = proxy.Encrypt(serverInfo, query, serverProto) - if err != nil { - pluginsState.returnCode = PluginsReturnCodeParseError - pluginsState.ApplyLoggingPlugins(&proxy.pluginsGlobals) - return response - } - response, err = proxy.exchangeWithTCPServer(serverInfo, sharedKey, encryptedQuery, clientNonce) - } - } else { - response, err = proxy.exchangeWithTCPServer(serverInfo, sharedKey, encryptedQuery, clientNonce) - } - if err != nil { - if stale, ok := pluginsState.sessionData["stale"]; ok { - dlog.Debug("Serving stale response") - response, err = (stale.(*dns.Msg)).Pack() - } - } - if err != nil { - if neterr, ok := err.(net.Error); ok && neterr.Timeout() { - pluginsState.returnCode = PluginsReturnCodeServerTimeout - } else { - pluginsState.returnCode = PluginsReturnCodeNetworkError - } - pluginsState.ApplyLoggingPlugins(&proxy.pluginsGlobals) - serverInfo.noticeFailure(proxy) - return response - } - } else if serverInfo.Proto == stamps.StampProtoTypeDoH { - tid := TransactionID(query) - SetTransactionID(query, 0) - serverInfo.noticeBegin(proxy) - serverResponse, _, tls, _, err := proxy.xTransport.DoHQuery(serverInfo.useGet, serverInfo.URL, query, proxy.timeout) - SetTransactionID(query, tid) - - if err != nil || tls == nil || !tls.HandshakeComplete { - if stale, ok := pluginsState.sessionData["stale"]; ok { - dlog.Debug("Serving stale response") - response, err = (stale.(*dns.Msg)).Pack() - } - } - if err != nil { - pluginsState.returnCode = PluginsReturnCodeNetworkError - pluginsState.ApplyLoggingPlugins(&proxy.pluginsGlobals) - serverInfo.noticeFailure(proxy) - return response - } - if response == nil { - response = serverResponse - } - if len(response) >= MinDNSPacketSize { - SetTransactionID(response, tid) - } - } else if serverInfo.Proto == stamps.StampProtoTypeODoHTarget { - tid := TransactionID(query) - if len(serverInfo.odohTargetConfigs) == 0 { - return response - } - target := serverInfo.odohTargetConfigs[rand.Intn(len(serverInfo.odohTargetConfigs))] - odohQuery, err := target.encryptQuery(query) - if err != nil { - dlog.Errorf("Failed to encrypt query for [%v]", serverName) - response = nil - } else { - targetURL := serverInfo.URL - if serverInfo.Relay != nil && serverInfo.Relay.ODoH != nil { - targetURL = serverInfo.Relay.ODoH.URL - } - responseBody, responseCode, _, _, err := proxy.xTransport.ObliviousDoHQuery(serverInfo.useGet, targetURL, odohQuery.odohMessage, proxy.timeout) - if err == nil && len(responseBody) > 0 && responseCode == 200 { - response, err = odohQuery.decryptResponse(responseBody) - if err != nil { - dlog.Warnf("Failed to decrypt response from [%v]", serverName) - response = nil - } - } else if responseCode == 401 || (responseCode == 200 && len(responseBody) == 0) { - if responseCode == 200 { - dlog.Warnf("ODoH relay for [%v] is buggy and returns a 200 status code instead of 401 after a key update", serverInfo.Name) - } - dlog.Infof("Forcing key update for [%v]", serverInfo.Name) - for _, registeredServer := range proxy.serversInfo.registeredServers { - if registeredServer.name == serverInfo.Name { - if err = proxy.serversInfo.refreshServer(proxy, registeredServer.name, registeredServer.stamp); err != nil { - // Failed to refresh the proxy server information. - dlog.Noticef("Key update failed for [%v]", serverName) - serverInfo.noticeFailure(proxy) - clocksmith.Sleep(10 * time.Second) - } - break - } - } - response = nil - } else { - dlog.Warnf("Failed to receive successful response from [%v]", serverName) - } - } - - if len(response) >= MinDNSPacketSize { - SetTransactionID(response, tid) - } else if response == nil { - pluginsState.returnCode = PluginsReturnCodeNetworkError - pluginsState.ApplyLoggingPlugins(&proxy.pluginsGlobals) - serverInfo.noticeFailure(proxy) - return response - } - } else { - dlog.Fatal("Unsupported protocol") - } - if len(response) < MinDNSPacketSize || len(response) > MaxDNSPacketSize { - pluginsState.returnCode = PluginsReturnCodeParseError - pluginsState.ApplyLoggingPlugins(&proxy.pluginsGlobals) - serverInfo.noticeFailure(proxy) - return response - } - response, err = pluginsState.ApplyResponsePlugins(&proxy.pluginsGlobals, response, ttl) - if err != nil { - pluginsState.returnCode = PluginsReturnCodeParseError - pluginsState.ApplyLoggingPlugins(&proxy.pluginsGlobals) - serverInfo.noticeFailure(proxy) - return response - } - if pluginsState.action == PluginsActionDrop { - pluginsState.returnCode = PluginsReturnCodeDrop - pluginsState.ApplyLoggingPlugins(&proxy.pluginsGlobals) - return response - } - if pluginsState.synthResponse != nil { - response, err = pluginsState.synthResponse.PackBuffer(response) - if err != nil { - pluginsState.returnCode = PluginsReturnCodeParseError - pluginsState.ApplyLoggingPlugins(&proxy.pluginsGlobals) - return response - } - } - if rcode := Rcode(response); rcode == dns.RcodeServerFailure { // SERVFAIL - if pluginsState.dnssec { - dlog.Debug("A response had an invalid DNSSEC signature") - } else { - dlog.Infof("A response with status code 2 was received - this is usually a temporary, remote issue with the configuration of the domain name") - serverInfo.noticeFailure(proxy) - } - } else { - serverInfo.noticeSuccess(proxy) - } - } - if len(response) < MinDNSPacketSize || len(response) > MaxDNSPacketSize { - if len(response) == 0 { - pluginsState.returnCode = PluginsReturnCodeNotReady - } else { - pluginsState.returnCode = PluginsReturnCodeParseError - } - pluginsState.ApplyLoggingPlugins(&proxy.pluginsGlobals) - if serverInfo != nil { - serverInfo.noticeFailure(proxy) - } - return response - } - if clientProto == "udp" { - if len(response) > pluginsState.maxUnencryptedUDPSafePayloadSize { - response, err = TruncatedResponse(response) - if err != nil { - pluginsState.returnCode = PluginsReturnCodeParseError - pluginsState.ApplyLoggingPlugins(&proxy.pluginsGlobals) - return response - } - } - clientPc.(net.PacketConn).WriteTo(response, *clientAddr) - if HasTCFlag(response) { - proxy.questionSizeEstimator.blindAdjust() - } else { - proxy.questionSizeEstimator.adjust(ResponseOverhead + len(response)) - } - } else if clientProto == "tcp" { - response, err = PrefixWithSize(response) - if err != nil { - pluginsState.returnCode = PluginsReturnCodeParseError - pluginsState.ApplyLoggingPlugins(&proxy.pluginsGlobals) - if serverInfo != nil { - serverInfo.noticeFailure(proxy) - } - return response - } - if clientPc != nil { - clientPc.Write(response) - } - } - pluginsState.ApplyLoggingPlugins(&proxy.pluginsGlobals) - - return response -} - -func NewProxy() *Proxy { - return &Proxy{ - serversInfo: NewServersInfo(), - } -} diff --git a/dappnode_package.json b/dappnode_package.json index 131008f..1da7c92 100644 --- a/dappnode_package.json +++ b/dappnode_package.json @@ -1,7 +1,7 @@ { "name": "bind.dnp.dappnode.eth", "version": "0.2.11", - "upstreamVersion": "2.1.5", + "upstreamVersion": "2.1.20", "shortDescription": "DNS/ENS forwarder for Dappnode", "description": "Dappnode Core Package responsible for forwarding the DNS and ENS requests to the proper resolver", "architectures": ["linux/amd64", "linux/arm64"], diff --git a/docker-compose.yml b/docker-compose.yml index 2cf1ce7..60298c4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: build: context: bind args: - UPSTREAM_VERSION: 2.1.5 + UPSTREAM_VERSION: 2.1.20 restart: always networks: dncore_network: From 51cb241135615ed3159773542e6fed5b58a54d06 Mon Sep 17 00:00:00 2001 From: 3alpha <15694175+3alpha@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:28:13 +0200 Subject: [PATCH 2/2] update deps --- .github/workflows/main.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ed96dbd..706f03a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,11 +17,17 @@ jobs: steps: - name: Checkout uses: actions/checkout@v7.0.1 + - name: Setup node + uses: actions/setup-node@v7.1.0 + with: + node-version: "26.12.1" + - name: Check publishing CLI + run: npx --yes "@dappnode/dappnodesdk@${DAPPNODE_SDK_VERSION}" publish --help - run: docker compose build --pull --no-cache release: name: Release - runs-on: ubuntu-latest + runs-on: ipfs-dev-gateway if: github.event_name == 'push' steps: - uses: actions/checkout@v7.0.1