Skip to content

Commit eda26b4

Browse files
committed
Support $DHCP and $BOOTSTRAP keywords in forwarding rules
Ideally, that should also be supported by the captive portal handler. Great work by @lifenjoiner Fixes #2460
1 parent cd3cb2e commit eda26b4

5 files changed

Lines changed: 172 additions & 40 deletions

File tree

dnscrypt-proxy/example-forwarding-rules.txt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,31 @@
77
## <domain> <server address>[:port] [, <server address>[:port]...]
88
## IPv6 addresses can be specified by enclosing the address in square brackets.
99

10+
## The following keywords can also be used instead of a server address:
11+
## $BOOTSTRAP to use the default bootstrap resolvers
12+
## $DHCP to use the default DNS resolvers provided by the DHCP server
13+
1014
## In order to enable this feature, the "forwarding_rules" property needs to
1115
## be set to this file name inside the main configuration file.
1216

1317
## Blocking IPv6 may prevent local devices from being discovered.
1418
## If this happens, set `block_ipv6` to `false` in the main config file.
1519

16-
## Forward *.lan, *.local, *.home, *.home.arpa, *.internal and *.localdomain to 192.168.1.1
20+
## Forward *.lan, *.home, *.home.arpa, and *.localdomain to 192.168.1.1
1721
# lan 192.168.1.1
18-
# local 192.168.1.1
1922
# home 192.168.1.1
2023
# home.arpa 192.168.1.1
21-
# internal 192.168.1.1
2224
# localdomain 192.168.1.1
2325
# 192.in-addr.arpa 192.168.1.1
2426

27+
## Forward *.local to the resolvers provided by the DHCP server
28+
# local $DHCP
29+
30+
## Forward *.internal to 192.168.1.1, and if it doesn't work, to the
31+
## DNS from the local DHCP server, and if it still doesn't work, to the
32+
## bootstrap resolvers
33+
# internal 192.168.1.1,$DHCP,$BOOTSTRAP
34+
2535
## Forward queries for example.com and *.example.com to 9.9.9.9 and 8.8.8.8
2636
# example.com 9.9.9.9,8.8.8.8
2737

dnscrypt-proxy/plugin_forward.go

Lines changed: 153 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,32 @@ import (
77
"strings"
88

99
"github.com/jedisct1/dlog"
10+
"github.com/lifenjoiner/dhcpdns"
1011
"github.com/miekg/dns"
1112
)
1213

13-
type PluginForwardEntry struct {
14-
domain string
14+
type SearchSequenceItemType int
15+
16+
const (
17+
Explicit SearchSequenceItemType = iota
18+
Bootstrap
19+
DHCP
20+
)
21+
22+
type SearchSequenceItem struct {
23+
typ SearchSequenceItemType
1524
servers []string
1625
}
1726

27+
type PluginForwardEntry struct {
28+
domain string
29+
sequence []SearchSequenceItem
30+
}
31+
1832
type PluginForward struct {
19-
forwardMap []PluginForwardEntry
33+
forwardMap []PluginForwardEntry
34+
bootstrapResolvers []string
35+
dhcpdns []*dhcpdns.Detector
2036
}
2137

2238
func (plugin *PluginForward) Name() string {
@@ -29,6 +45,11 @@ func (plugin *PluginForward) Description() string {
2945

3046
func (plugin *PluginForward) Init(proxy *Proxy) error {
3147
dlog.Noticef("Loading the set of forwarding rules from [%s]", proxy.forwardFile)
48+
49+
if proxy.xTransport != nil {
50+
plugin.bootstrapResolvers = proxy.xTransport.bootstrapResolvers
51+
}
52+
3253
lines, err := ReadTextFile(proxy.forwardFile)
3354
if err != nil {
3455
return err
@@ -46,27 +67,77 @@ func (plugin *PluginForward) Init(proxy *Proxy) error {
4667
)
4768
}
4869
domain = strings.ToLower(domain)
49-
var servers []string
70+
requiresDHCP := false
71+
var sequence []SearchSequenceItem
5072
for _, server := range strings.Split(serversStr, ",") {
5173
server = strings.TrimSpace(server)
52-
server = strings.TrimPrefix(server, "[")
53-
server = strings.TrimSuffix(server, "]")
54-
if ip := net.ParseIP(server); ip != nil {
55-
if ip.To4() != nil {
56-
server = fmt.Sprintf("%s:%d", server, 53)
74+
switch server {
75+
case "$BOOTSTRAP":
76+
if len(plugin.bootstrapResolvers) == 0 {
77+
return fmt.Errorf(
78+
"Syntax error for a forwarding rule at line %d. No bootstrap resolvers available",
79+
1+lineNo,
80+
)
81+
}
82+
if len(sequence) > 0 && sequence[len(sequence)-1].typ == Bootstrap {
83+
// Ignore repetitions
5784
} else {
58-
server = fmt.Sprintf("[%s]:%d", server, 53)
85+
sequence = append(sequence, SearchSequenceItem{typ: Bootstrap})
86+
dlog.Infof("Forwarding [%s] to the bootstrap servers", domain)
5987
}
88+
case "$DHCP":
89+
if len(sequence) > 0 && sequence[len(sequence)-1].typ == DHCP {
90+
// Ignore repetitions
91+
} else {
92+
sequence = append(sequence, SearchSequenceItem{typ: Bootstrap})
93+
dlog.Infof("Forwarding [%s] to the DHCP servers", domain)
94+
}
95+
requiresDHCP = true
96+
default:
97+
if strings.HasPrefix(server, "$") {
98+
dlog.Criticalf("Unknown keyword [%s] at line %d", server, 1+lineNo)
99+
continue
100+
}
101+
server = strings.TrimPrefix(server, "[")
102+
server = strings.TrimSuffix(server, "]")
103+
if ip := net.ParseIP(server); ip != nil {
104+
if ip.To4() != nil {
105+
server = fmt.Sprintf("%s:%d", server, 53)
106+
} else {
107+
server = fmt.Sprintf("[%s]:%d", server, 53)
108+
}
109+
}
110+
idxServers := -1
111+
for i, item := range sequence {
112+
if item.typ == Explicit {
113+
idxServers = i
114+
}
115+
}
116+
if idxServers == -1 {
117+
sequence = append(sequence, SearchSequenceItem{typ: Explicit, servers: []string{server}})
118+
} else {
119+
sequence[idxServers].servers = append(sequence[idxServers].servers, server)
120+
}
121+
dlog.Infof("Forwarding [%s] to [%s]", domain, server)
60122
}
61-
dlog.Infof("Forwarding [%s] to %s", domain, server)
62-
servers = append(servers, server)
63123
}
64-
if len(servers) == 0 {
65-
continue
124+
if requiresDHCP {
125+
if proxy.SourceIPv6 {
126+
dlog.Info("Starting a DHCP/DNS detector for IPv6")
127+
d6 := &dhcpdns.Detector{RemoteIPPort: "[2001:DB8::53]:80"}
128+
go d6.Serve(9, 10)
129+
plugin.dhcpdns = append(plugin.dhcpdns, d6)
130+
}
131+
if proxy.SourceIPv4 {
132+
dlog.Info("Starting a DHCP/DNS detector for IPv4")
133+
d4 := &dhcpdns.Detector{RemoteIPPort: "192.0.2.53:80"}
134+
go d4.Serve(9, 10)
135+
plugin.dhcpdns = append(plugin.dhcpdns, d4)
136+
}
66137
}
67138
plugin.forwardMap = append(plugin.forwardMap, PluginForwardEntry{
68-
domain: domain,
69-
servers: servers,
139+
domain: domain,
140+
sequence: sequence,
70141
})
71142
}
72143
return nil
@@ -83,7 +154,7 @@ func (plugin *PluginForward) Reload() error {
83154
func (plugin *PluginForward) Eval(pluginsState *PluginsState, msg *dns.Msg) error {
84155
qName := pluginsState.qName
85156
qNameLen := len(qName)
86-
var servers []string
157+
var sequence []SearchSequenceItem
87158
for _, candidate := range plugin.forwardMap {
88159
candidateLen := len(candidate.domain)
89160
if candidateLen > qNameLen {
@@ -92,33 +163,78 @@ func (plugin *PluginForward) Eval(pluginsState *PluginsState, msg *dns.Msg) erro
92163
if (qName[qNameLen-candidateLen:] == candidate.domain &&
93164
(candidateLen == qNameLen || (qName[qNameLen-candidateLen-1] == '.'))) ||
94165
(candidate.domain == ".") {
95-
servers = candidate.servers
166+
sequence = candidate.sequence
96167
break
97168
}
98169
}
99-
if len(servers) == 0 {
170+
if len(sequence) == 0 {
100171
return nil
101172
}
102-
server := servers[rand.Intn(len(servers))]
103-
pluginsState.serverName = server
104-
client := dns.Client{Net: pluginsState.serverProto, Timeout: pluginsState.timeout}
105-
respMsg, _, err := client.Exchange(msg, server)
106-
if err != nil {
107-
return err
108-
}
109-
if respMsg.Truncated {
110-
client.Net = "tcp"
173+
var err error
174+
var respMsg *dns.Msg
175+
var tries = 4
176+
for _, item := range sequence {
177+
var server string
178+
switch item.typ {
179+
case Explicit:
180+
server = item.servers[rand.Intn(len(item.servers))]
181+
pluginsState.serverName = server
182+
case Bootstrap:
183+
server = plugin.bootstrapResolvers[rand.Intn(len(plugin.bootstrapResolvers))]
184+
pluginsState.serverName = "[BOOTSTRAP]"
185+
case DHCP:
186+
const maxInconsistency = 9
187+
for _, dhcpdns := range plugin.dhcpdns {
188+
inconsistency, ip, dhcpDNS, err := dhcpdns.Status()
189+
if err != nil && ip != "" && inconsistency > maxInconsistency {
190+
dhcpDNS = nil
191+
}
192+
if len(dhcpDNS) > 0 {
193+
server = net.JoinHostPort(dhcpDNS[rand.Intn(len(dhcpDNS))].String(), "53")
194+
break
195+
}
196+
}
197+
if len(server) == 0 {
198+
dlog.Warn("DHCP didn't provide any DNS server")
199+
continue
200+
}
201+
pluginsState.serverName = "[DHCP]"
202+
}
203+
if len(server) == 0 {
204+
continue
205+
}
206+
207+
if tries == 0 {
208+
break
209+
}
210+
tries--
211+
dlog.Debugf("Forwarding [%s] to [%s]", qName, server)
212+
client := dns.Client{Net: pluginsState.serverProto, Timeout: pluginsState.timeout}
111213
respMsg, _, err = client.Exchange(msg, server)
112214
if err != nil {
113-
return err
215+
continue
114216
}
217+
if respMsg.Truncated {
218+
client.Net = "tcp"
219+
respMsg, _, err = client.Exchange(msg, server)
220+
if err != nil {
221+
continue
222+
}
223+
}
224+
if len(sequence) > 0 {
225+
switch respMsg.Rcode {
226+
case dns.RcodeNameError, dns.RcodeRefused, dns.RcodeNotAuth:
227+
continue
228+
}
229+
}
230+
if edns0 := respMsg.IsEdns0(); edns0 == nil || !edns0.Do() {
231+
respMsg.AuthenticatedData = false
232+
}
233+
respMsg.Id = msg.Id
234+
pluginsState.synthResponse = respMsg
235+
pluginsState.action = PluginsActionSynth
236+
pluginsState.returnCode = PluginsReturnCodeForward
237+
return nil
115238
}
116-
if edns0 := respMsg.IsEdns0(); edns0 == nil || !edns0.Do() {
117-
respMsg.AuthenticatedData = false
118-
}
119-
respMsg.Id = msg.Id
120-
pluginsState.synthResponse = respMsg
121-
pluginsState.action = PluginsActionSynth
122-
pluginsState.returnCode = PluginsReturnCodeForward
123-
return nil
239+
return err
124240
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ require (
1717
github.com/jedisct1/xsecretbox v0.0.0-20241212092125-3afc4917ac41
1818
github.com/k-sone/critbitgo v1.4.0
1919
github.com/kardianos/service v1.2.2
20+
github.com/lifenjoiner/dhcpdns v0.0.6
2021
github.com/miekg/dns v1.1.62
2122
github.com/opencoff/go-sieve v0.2.1
2223
github.com/powerman/check v1.8.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ github.com/k-sone/critbitgo v1.4.0 h1:l71cTyBGeh6X5ATh6Fibgw3+rtNT80BA0uNNWgkPrb
5151
github.com/k-sone/critbitgo v1.4.0/go.mod h1:7E6pyoyADnFxlUBEKcnfS49b7SUAQGMK+OAp/UQvo0s=
5252
github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60=
5353
github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
54+
github.com/lifenjoiner/dhcpdns v0.0.6 h1:rn4Y5RRR5sgQ6RjWenwhA7i/uHzHW9hbZpCobA4CAJs=
55+
github.com/lifenjoiner/dhcpdns v0.0.6/go.mod h1:BixeaGeafYzDIuDCYIUbSOdi4m+TScpzI9cZGYgzgSk=
5456
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
5557
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
5658
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=

vendor/modules.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ github.com/k-sone/critbitgo
6565
# github.com/kardianos/service v1.2.2
6666
## explicit; go 1.12
6767
github.com/kardianos/service
68+
# github.com/lifenjoiner/dhcpdns v0.0.6
69+
## explicit; go 1.20
70+
github.com/lifenjoiner/dhcpdns
6871
# github.com/miekg/dns v1.1.62
6972
## explicit; go 1.19
7073
github.com/miekg/dns

0 commit comments

Comments
 (0)