mirror of
https://github.com/folbricht/routedns.git
synced 2025-12-30 06:00:14 -06:00
Implement 'query-log' (#410)
* Implement 'query-log' * use slog to produce logs * Support JSON output for query-log
This commit is contained in:
@@ -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
|
||||
|
||||
14
cmd/routedns/example-config/query-log.toml
Normal file
14
cmd/routedns/example-config/query-log.toml
Normal 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"
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
97
query-log.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user