Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Unified patch context prefixes are followed by upstream Go indentation.
bind/custom/patches/*.patch -whitespace
18 changes: 12 additions & 6 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,25 @@ jobs:
if: github.event_name != 'push'
steps:
- name: Checkout
uses: actions/checkout@v4
- run: docker compose build
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@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:
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release-on-demanda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 12 additions & 6 deletions bind/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 && \
Expand Down
178 changes: 178 additions & 0 deletions bind/custom/patches/dappnode-forwarding.patch
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading