Skip to content

Commit 634168b

Browse files
committed
Merge branch 'master' of github.com:DNSCrypt/dnscrypt-proxy
2 parents f270cfc + 96f285e commit 634168b

282 files changed

Lines changed: 22910 additions & 41078 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ChangeLog

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
# Version 2.1.14 (not released yet)
2+
3+
# Version 2.1.13
4+
- Fixed race conditions in WebSocket handling for the monitoring dashboard,
5+
improving stability and preventing potential crashes.
6+
- Manual configuration reload via SIGHUP is now supported regardless of the
7+
hot-reload setting, providing more flexibility for system administrators.
8+
- Fixed a regression in IP prefix matching for allow/block lists that could
9+
cause incorrect filtering behavior.
10+
- The monitoring dashboard now properly displays blocked queries counter and
11+
tracks blocked queries in the UI.
12+
- Improved error handling in the cache plugin initialization.
13+
- Enhanced the forward plugin to return the last valid response when
14+
encountering only errors, improving resilience.
15+
- Fixed various UI issues including scrolling behavior, WebSocket reconnection
16+
handling, and response time calculations.
17+
- Updated the example configuration with current Quad9 source URLs.
18+
- The generate-domains-blocklist script now handles poor network conditions
19+
more gracefully.
20+
- Improved handling of DNS64 trampoline queries to prevent potential issues.
21+
122
# Version 2.1.12
223
- A new Weighted Power of Two (WP2) load balancing strategy has been
324
implemented as the default, providing improved distribution across resolvers.

dnscrypt-proxy/common.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@ import (
44
"bytes"
55
"encoding/binary"
66
"errors"
7+
"fmt"
8+
"io"
79
"net"
810
"os"
911
"strconv"
1012
"strings"
1113
"sync"
14+
"time"
1215
"unicode"
16+
17+
iradix "github.com/hashicorp/go-immutable-radix"
18+
"github.com/jedisct1/dlog"
1319
)
1420

1521
type CryptoConstruction uint16
@@ -171,3 +177,151 @@ func ReadTextFile(filename string) (string, error) {
171177
}
172178

173179
func isDigit(b byte) bool { return b >= '0' && b <= '9' }
180+
181+
// ExtractClientIPStr extracts client IP string from pluginsState based on protocol
182+
func ExtractClientIPStr(pluginsState *PluginsState) (string, bool) {
183+
switch pluginsState.clientProto {
184+
case "udp":
185+
return (*pluginsState.clientAddr).(*net.UDPAddr).IP.String(), true
186+
case "tcp", "local_doh":
187+
return (*pluginsState.clientAddr).(*net.TCPAddr).IP.String(), true
188+
default:
189+
return "", false
190+
}
191+
}
192+
193+
// FormatLogLine formats a log line based on the specified format (tsv or ltsv)
194+
func FormatLogLine(format, clientIP, qName, reason string, additionalFields ...string) (string, error) {
195+
if format == "tsv" {
196+
now := time.Now()
197+
year, month, day := now.Date()
198+
hour, minute, second := now.Clock()
199+
tsStr := fmt.Sprintf("[%d-%02d-%02d %02d:%02d:%02d]", year, int(month), day, hour, minute, second)
200+
201+
line := fmt.Sprintf("%s\t%s\t%s\t%s", tsStr, clientIP, StringQuote(qName), StringQuote(reason))
202+
for _, field := range additionalFields {
203+
line += fmt.Sprintf("\t%s", StringQuote(field))
204+
}
205+
return line + "\n", nil
206+
} else if format == "ltsv" {
207+
line := fmt.Sprintf("time:%d\thost:%s\tqname:%s\tmessage:%s", time.Now().Unix(), clientIP, StringQuote(qName), StringQuote(reason))
208+
209+
// For LTSV format, additional fields are added with specific labels
210+
for i, field := range additionalFields {
211+
if i == 0 {
212+
line += fmt.Sprintf("\tip:%s", StringQuote(field))
213+
} else {
214+
line += fmt.Sprintf("\tfield%d:%s", i, StringQuote(field))
215+
}
216+
}
217+
return line + "\n", nil
218+
}
219+
return "", fmt.Errorf("unexpected log format: [%s]", format)
220+
}
221+
222+
// WritePluginLog writes a log entry for plugin actions
223+
func WritePluginLog(logger io.Writer, format, clientIP, qName, reason string, additionalFields ...string) error {
224+
if logger == nil {
225+
return errors.New("Log file not initialized")
226+
}
227+
228+
line, err := FormatLogLine(format, clientIP, qName, reason, additionalFields...)
229+
if err != nil {
230+
return err
231+
}
232+
233+
_, err = logger.Write([]byte(line))
234+
return err
235+
}
236+
237+
// ParseTimeBasedRule parses a rule line that may contain time-based restrictions (@timerange)
238+
func ParseTimeBasedRule(line string, lineNo int, allWeeklyRanges *map[string]WeeklyRanges) (rulePart string, weeklyRanges *WeeklyRanges, err error) {
239+
parts := strings.Split(line, "@")
240+
timeRangeName := ""
241+
242+
if len(parts) == 2 {
243+
rulePart = strings.TrimSpace(parts[0])
244+
timeRangeName = strings.TrimSpace(parts[1])
245+
} else if len(parts) > 2 {
246+
return "", nil, fmt.Errorf("syntax error at line %d -- Unexpected @ character", 1+lineNo)
247+
} else {
248+
rulePart = line
249+
}
250+
251+
if len(timeRangeName) > 0 {
252+
if weeklyRangesX, ok := (*allWeeklyRanges)[timeRangeName]; ok {
253+
weeklyRanges = &weeklyRangesX
254+
} else {
255+
return "", nil, fmt.Errorf("time range [%s] not found at line %d", timeRangeName, 1+lineNo)
256+
}
257+
}
258+
259+
return rulePart, weeklyRanges, nil
260+
}
261+
262+
// ParseIPRule parses and validates an IP rule line
263+
func ParseIPRule(line string, lineNo int) (cleanLine string, trailingStar bool, err error) {
264+
ip := net.ParseIP(line)
265+
trailingStar = strings.HasSuffix(line, "*")
266+
267+
if len(line) < 2 || (ip != nil && trailingStar) {
268+
return "", false, fmt.Errorf("suspicious IP rule [%s] at line %d", line, lineNo)
269+
}
270+
271+
cleanLine = line
272+
if trailingStar {
273+
cleanLine = cleanLine[:len(cleanLine)-1]
274+
}
275+
if strings.HasSuffix(cleanLine, ":") || strings.HasSuffix(cleanLine, ".") {
276+
cleanLine = cleanLine[:len(cleanLine)-1]
277+
}
278+
if len(cleanLine) == 0 {
279+
return "", false, fmt.Errorf("empty IP rule at line %d", lineNo)
280+
}
281+
if strings.Contains(cleanLine, "*") {
282+
return "", false, fmt.Errorf("invalid rule: [%s] - wildcards can only be used as a suffix at line %d", line, lineNo)
283+
}
284+
285+
return strings.ToLower(cleanLine), trailingStar, nil
286+
}
287+
288+
// ProcessConfigLines processes configuration file lines, calling the processor function for each non-empty line
289+
func ProcessConfigLines(lines string, processor func(line string, lineNo int) error) error {
290+
for lineNo, line := range strings.Split(lines, "\n") {
291+
line = TrimAndStripInlineComments(line)
292+
if len(line) == 0 {
293+
continue
294+
}
295+
if err := processor(line, lineNo); err != nil {
296+
return err
297+
}
298+
}
299+
return nil
300+
}
301+
302+
// LoadIPRules loads IP rules from text lines into radix tree and map structures
303+
func LoadIPRules(lines string, prefixes *iradix.Tree, ips map[string]interface{}) (*iradix.Tree, error) {
304+
err := ProcessConfigLines(lines, func(line string, lineNo int) error {
305+
cleanLine, trailingStar, lineErr := ParseIPRule(line, lineNo)
306+
if lineErr != nil {
307+
dlog.Error(lineErr)
308+
return nil // Continue processing (matching existing behavior)
309+
}
310+
311+
if trailingStar {
312+
prefixes, _, _ = prefixes.Insert([]byte(cleanLine), 0)
313+
} else {
314+
ips[cleanLine] = true
315+
}
316+
return nil
317+
})
318+
return prefixes, err
319+
}
320+
321+
// InitializePluginLogger initializes a logger for a plugin if the log file is configured
322+
func InitializePluginLogger(logFile, format string, maxSize, maxAge, maxBackups int) (io.Writer, string) {
323+
if len(logFile) > 0 {
324+
return Logger(maxSize, maxAge, maxBackups, logFile), format
325+
}
326+
return nil, ""
327+
}

dnscrypt-proxy/config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
461461
if len(proxy.userName) > 0 && !proxy.child {
462462
proxy.dropPrivilege(proxy.userName, FileDescriptors)
463463
return errors.New(
464-
"Dropping privileges is not supporting on this operating system. Unset `user_name` in the configuration file",
464+
"Dropping privileges is not supported on this operating system. Unset `user_name` in the configuration file",
465465
)
466466
}
467467

@@ -471,7 +471,7 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
471471
return err
472472
}
473473
if len(proxy.registeredServers) == 0 {
474-
return errors.New("None of the servers listed in the server_names list was found in the configured sources.")
474+
return errors.New("None of the servers listed in the server_names list were found in the configured sources.")
475475
}
476476
}
477477

dnscrypt-proxy/example-allowed-names.txt

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@
77
##
88
## Example of valid patterns:
99
##
10-
## ads.* | matches anything with an "ads." prefix
11-
## *.example.com | matches example.com and all names within that zone such as www.example.com
12-
## example.com | identical to the above
13-
## =example.com | allows example.com but not *.example.com
14-
## *sex* | matches any name containing that substring
15-
## ads[0-9]* | matches "ads" followed by one or more digits
16-
## ads*.example* | *, ? and [] can be used anywhere, but prefixes/suffixes are faster
10+
## ads.* | matches anything with an "ads." prefix
11+
## *.example.com | matches example.com and all names within that zone such as www.example.com
12+
## example.com | identical to the above
13+
## =example.com | allows example.com but not *.example.com
14+
## [a-z0-9\-_]*.example.com | allows *.example.com but not example.com
15+
## *sex* | matches any name containing that substring
16+
## ads[0-9]* | matches "ads" followed by one or more digits
17+
## ads*.example* | *, ? and [] can be used anywhere, but prefixes/suffixes are faster
1718

1819

1920
# That one may be blocked due to 'tracker' being in the name.

dnscrypt-proxy/example-blocked-names.txt

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@
77
##
88
## Example of valid patterns:
99
##
10-
## ads.* | matches anything with an "ads." prefix
11-
## *.example.com | matches example.com and all names within that zone such as www.example.com
12-
## example.com | identical to the above
13-
## =example.com | block example.com but not *.example.com
14-
## *sex* | matches any name containing that substring
15-
## ads[0-9]* | matches "ads" followed by one or more digits
16-
## ads*.example* | *, ? and [] can be used anywhere, but prefixes/suffixes are faster
10+
## ads.* | matches anything with an "ads." prefix
11+
## *.example.com | matches example.com and all names within that zone such as www.example.com
12+
## example.com | identical to the above
13+
## =example.com | blocks example.com but not *.example.com
14+
## [a-z0-9\-_]*.example.com | blocks *.example.com but not example.com
15+
## *sex* | matches any name containing that substring
16+
## ads[0-9]* | matches "ads" followed by one or more digits
17+
## ads*.example* | *, ? and [] can be used anywhere, but prefixes/suffixes are faster
1718

1819
ad.*
1920
ads.*

dnscrypt-proxy/example-dnscrypt-proxy.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ bootstrap_resolvers = ['9.9.9.11:53', '8.8.8.8:53']
346346
## to the system DNS
347347
##
348348
## (*) this is incompatible with systemd sockets.
349-
## `listen_addrs` must not be empty.
349+
## `listen_addresses` must not be empty.
350350

351351
ignore_system_dns = true
352352

@@ -790,7 +790,7 @@ prefix = ''
790790
### Quad9
791791

792792
# [sources.quad9-resolvers]
793-
# urls = ['https://www.quad9.net/quad9-resolvers.md']
793+
# urls = ['https://quad9.net/dnscrypt/quad9-resolvers.md', 'https://raw.githubusercontent.com/Quad9DNS/dnscrypt-settings/main/dnscrypt/quad9-resolvers.md']
794794
# minisign_key = 'RWQBphd2+f6eiAqBsvDZEBXBGHQBJfeG6G+wJPPKxCZMoEQYpmoysKUN'
795795
# cache_file = 'quad9-resolvers.md'
796796
# prefix = 'quad9-'
@@ -851,7 +851,7 @@ fragments_blocked = [
851851

852852
[doh_client_x509_auth]
853853

854-
## Use a X509 certificate to authenticate yourself when connecting to DoH servers.
854+
## Use an X509 certificate to authenticate yourself when connecting to DoH servers.
855855
## This is only useful if you are operating your own, private DoH server(s).
856856
## 'creds' maps servers to certificates, and supports multiple entries.
857857
## If you are not using the standard root CA, an optional "root_ca"

dnscrypt-proxy/hot_reload.go

Lines changed: 18 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,46 @@
11
package main
22

33
import (
4-
"os"
5-
"os/signal"
6-
"syscall"
7-
84
"github.com/jedisct1/dlog"
95
)
106

117
// InitHotReload sets up hot-reloading for configuration files
128
func (proxy *Proxy) InitHotReload() error {
13-
// Check if hot reload is enabled
14-
if !proxy.enableHotReload {
9+
// Check if hot reload is enabled and platform has SIGHUP
10+
if !proxy.enableHotReload && !HasSIGHUP {
1511
dlog.Notice("Hot reload is disabled")
1612
return nil
1713
}
1814

19-
dlog.Notice("Hot reload is enabled")
20-
21-
// Create a new configuration watcher
22-
configWatcher := NewConfigWatcher(1000) // Check every second
23-
2415
// Find plugins that support hot-reloading
2516
plugins := []Plugin{}
2617

2718
// Add query plugins
2819
proxy.pluginsGlobals.RLock()
2920
if proxy.pluginsGlobals.queryPlugins != nil {
30-
for _, plugin := range *proxy.pluginsGlobals.queryPlugins {
31-
plugins = append(plugins, plugin)
32-
}
21+
plugins = append(plugins, *proxy.pluginsGlobals.queryPlugins...)
3322
}
3423

3524
// Add response plugins
3625
if proxy.pluginsGlobals.responsePlugins != nil {
37-
for _, plugin := range *proxy.pluginsGlobals.responsePlugins {
38-
plugins = append(plugins, plugin)
39-
}
26+
plugins = append(plugins, *proxy.pluginsGlobals.responsePlugins...)
4027
}
4128
proxy.pluginsGlobals.RUnlock()
4229

30+
// Setup SIGHUP handler for manual reload
31+
setupSignalHandler(proxy, plugins)
32+
33+
// Check if hot reload is enabled
34+
if !proxy.enableHotReload {
35+
dlog.Notice("Hot reload is disabled")
36+
return nil
37+
}
38+
39+
dlog.Notice("Hot reload is enabled")
40+
41+
// Create a new configuration watcher
42+
configWatcher := NewConfigWatcher(1000) // Check every second
43+
4344
// Register plugins for config watching
4445
for _, plugin := range plugins {
4546
switch p := plugin.(type) {
@@ -100,32 +101,5 @@ func (proxy *Proxy) InitHotReload() error {
100101
}
101102
}
102103

103-
// Setup SIGHUP handler for manual reload
104-
setupSignalHandler(proxy, plugins)
105-
106104
return nil
107105
}
108-
109-
// setupSignalHandler sets up a SIGHUP handler to manually trigger reloads
110-
func setupSignalHandler(proxy *Proxy, plugins []Plugin) {
111-
sigChan := make(chan os.Signal, 1)
112-
signal.Notify(sigChan, syscall.SIGHUP)
113-
114-
go func() {
115-
for {
116-
sig := <-sigChan
117-
if sig == syscall.SIGHUP {
118-
dlog.Notice("Received SIGHUP signal, reloading configurations")
119-
120-
// Reload each plugin that supports hot-reloading
121-
for _, plugin := range plugins {
122-
if err := plugin.Reload(); err != nil {
123-
dlog.Errorf("Failed to reload plugin [%s]: %v", plugin.Name(), err)
124-
} else {
125-
dlog.Noticef("Successfully reloaded plugin [%s]", plugin.Name())
126-
}
127-
}
128-
}
129-
}
130-
}()
131-
}

dnscrypt-proxy/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
)
1616

1717
const (
18-
AppVersion = "2.1.12"
18+
AppVersion = "2.1.13"
1919
DefaultConfigFileName = "dnscrypt-proxy.toml"
2020
)
2121

0 commit comments

Comments
 (0)