Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement 'query-log' #410

Merged
merged 4 commits into from
Jan 14, 2025
Merged
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
5 changes: 5 additions & 0 deletions cmd/routedns/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions cmd/routedns/example-config/query-log.toml
Original file line number Diff line number Diff line change
@@ -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"
13 changes: 12 additions & 1 deletion cmd/routedns/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
26 changes: 26 additions & 0 deletions doc/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
97 changes: 97 additions & 0 deletions query-log.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading