Implement 'query-log' (#410)

* Implement 'query-log'

* use slog to produce logs

* Support JSON output for query-log
This commit is contained in:
Frank Olbricht
2025-01-14 06:46:54 +01:00
committed by GitHub
parent 763e75db6f
commit 3d75a4e7f7
5 changed files with 154 additions and 1 deletions

View File

@@ -60,6 +60,7 @@ type resolver struct {
//QUIC and DoH/3 configuration
Use0RTT bool `toml:"enable-0rtt"`
// URL for Oblivious DNS target
Target string `toml:"target"`
TargetConfig string `toml:"target-config"`
@@ -176,6 +177,10 @@ type group struct {
LogRequest bool `toml:"log-request"` // Logs request records to syslog
LogResponse bool `toml:"log-response"` // Logs response records to syslog
Verbose bool `toml:"verbose"` // When logging responses, include types that don't match the query type
// Query logging options
OutputFile string `toml:"output-file"` // Log filename or blank for STDOUT
OutputFormat string `toml:"output-format"` // "text" or "json"
}
// Block/Allowlist items for blocklist-v2

View File

@@ -0,0 +1,14 @@
[listeners.local-udp]
address = "127.0.0.1:53"
protocol = "udp"
resolver = "query-log"
[groups.query-log]
type = "query-log"
resolvers = ["cloudflare-dot"]
# output-file = "/tmp/query.log" # Logs are written to STDOUT if blank, uncomment to write to file
output-format = "text" # or "json"
[resolvers.cloudflare-dot]
address = "1.1.1.1:853"
protocol = "dot"

View File

@@ -830,7 +830,18 @@ func instantiateGroup(id string, g group, resolvers map[string]rdns.Resolver) er
LimitResolver: resolvers[g.LimitResolver],
}
resolvers[id] = rdns.NewRateLimiter(id, gr[0], opt)
case "query-log":
if len(gr) != 1 {
return fmt.Errorf("type query-log only supports one resolver in '%s'", id)
}
opt := rdns.QueryLogResolverOptions{
OutputFile: g.OutputFile,
OutputFormat: rdns.LogFormat(g.OutputFormat),
}
resolvers[id], err = rdns.NewQueryLogResolver(id, gr[0], opt)
if err != nil {
return fmt.Errorf("failed to initialize 'query-log': %w", err)
}
default:
return fmt.Errorf("unsupported group type '%s' for group '%s'", g.Type, id)
}

View File

@@ -38,6 +38,7 @@
- [Retrying Truncated Responses](#retrying-truncated-responses)
- [Request Deduplication](#request-deduplication)
- [Syslog](#syslog)
- [Qyery Log](#query-log)
- [Resolvers](#resolvers)
- [Plain DNS](#plain-dns-resolver)
- [DNS-over-TLS](#dns-over-tls-resolver)
@@ -1458,6 +1459,31 @@ log-response = true
Example config files: [syslog.toml](../cmd/routedns/example-config/syslog.toml)
### Query Log
The `query-log` element logs all DNS query details, including time, client IP, DNS question name, class and type. Logs can be written to a file or STDOUT.
#### Configuration
To enable query-logging, add an element with `type = "query-log"` in the groups section of the configuration.
Options:
- `output-file` - Name of the file to write logs to, leave blank for STDOUT. Logs are appended to the file and there is no rotation.
- `output-format` - Output format. Defaults to "text".
Examples:
```toml
[groups.query-log]
type = "query-log"
resolvers = ["cloudflare-dot"]
output-file = "/tmp/query.log"
output-format = "text"
```
Example config files: [syslog.toml](../cmd/routedns/example-config/query-log.toml)
## Resolvers
Resolvers forward queries to other DNS servers over the network and typically represent the end of one or many processing pipelines. Resolvers encode every query that is passed from listeners, modifiers, routers etc and send them to a DNS server without further processing. Like with other elements in the pipeline, resolvers requires a unique identifier to reference them from other elements. The following protocols are supported:

97
query-log.go Normal file
View File

@@ -0,0 +1,97 @@
package rdns
import (
"context"
"fmt"
"log/slog"
"os"
"github.com/miekg/dns"
)
// QueryLogResolver logs requests to STDOUT or file.
type QueryLogResolver struct {
id string
resolver Resolver
opt QueryLogResolverOptions
logger *slog.Logger
}
var _ Resolver = &QueryLogResolver{}
type QueryLogResolverOptions struct {
OutputFile string // Output filename, leave blank for STDOUT
OutputFormat LogFormat
}
type LogFormat string
const (
LogFormatText LogFormat = "text"
LogFormatJSON LogFormat = "json"
)
// NewQueryLogResolver returns a new instance of a QueryLogResolver.
func NewQueryLogResolver(id string, resolver Resolver, opt QueryLogResolverOptions) (*QueryLogResolver, error) {
w := os.Stdout
if opt.OutputFile != "" {
f, err := os.OpenFile(opt.OutputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return nil, err
}
w = f
}
handlerOpts := &slog.HandlerOptions{
ReplaceAttr: logReplaceAttr,
}
var logger *slog.Logger
switch opt.OutputFormat {
case "", LogFormatText:
logger = slog.New(slog.NewTextHandler(w, handlerOpts))
case LogFormatJSON:
logger = slog.New(slog.NewJSONHandler(w, handlerOpts))
default:
return nil, fmt.Errorf("invalid output format %q", opt.OutputFormat)
}
return &QueryLogResolver{
resolver: resolver,
logger: logger,
}, nil
}
// Resolve logs the query details and passes the query to the next resolver.
func (r *QueryLogResolver) Resolve(q *dns.Msg, ci ClientInfo) (*dns.Msg, error) {
question := q.Question[0]
attrs := []slog.Attr{
slog.String("source-ip", ci.SourceIP.String()),
slog.String("question-name", question.Name),
slog.String("question-class", dns.Class(question.Qclass).String()),
slog.String("question-type", dns.Type(question.Qtype).String()),
}
// Add ECS attributes if present
edns0 := q.IsEdns0()
if edns0 != nil {
// Find the ECS option
for _, opt := range edns0.Option {
ecs, ok := opt.(*dns.EDNS0_SUBNET)
if ok {
attrs = append(attrs, slog.String("ecs-addr", ecs.Address.String()))
}
}
}
r.logger.LogAttrs(context.Background(), slog.LevelInfo, "", attrs...)
return r.resolver.Resolve(q, ci)
}
func (r *QueryLogResolver) String() string {
return r.id
}
func logReplaceAttr(groups []string, a slog.Attr) slog.Attr {
if a.Key == "msg" || a.Key == "level" {
return slog.Attr{}
}
return a
}