mirror of https://github.com/0xERR0R/blocky.git
feat: add `queryLog.ignore.sudn` option to ignore SUDN responses
This commit is contained in:
parent
c56f0f91ca
commit
9d65b9395d
|
@ -4,6 +4,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/0xERR0R/blocky/log"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,6 +17,11 @@ type QueryLog struct {
|
||||||
CreationCooldown Duration `yaml:"creationCooldown" default:"2s"`
|
CreationCooldown Duration `yaml:"creationCooldown" default:"2s"`
|
||||||
Fields []QueryLogField `yaml:"fields"`
|
Fields []QueryLogField `yaml:"fields"`
|
||||||
FlushInterval Duration `yaml:"flushInterval" default:"30s"`
|
FlushInterval Duration `yaml:"flushInterval" default:"30s"`
|
||||||
|
Ignore QueryLogIgnore `yaml:"ignore"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryLogIgnore struct {
|
||||||
|
SUDN bool `yaml:"sudn" default:"false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaults implements `defaults.Setter`.
|
// SetDefaults implements `defaults.Setter`.
|
||||||
|
@ -43,6 +49,11 @@ func (c *QueryLog) LogConfig(logger *logrus.Entry) {
|
||||||
logger.Debugf("creationCooldown: %s", c.CreationCooldown)
|
logger.Debugf("creationCooldown: %s", c.CreationCooldown)
|
||||||
logger.Infof("flushInterval: %s", c.FlushInterval)
|
logger.Infof("flushInterval: %s", c.FlushInterval)
|
||||||
logger.Infof("fields: %s", c.Fields)
|
logger.Infof("fields: %s", c.Fields)
|
||||||
|
|
||||||
|
logger.Infof("ignore:")
|
||||||
|
log.WithIndent(logger, " ", func(e *logrus.Entry) {
|
||||||
|
logger.Infof("sudn: %t", c.Ignore.SUDN)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *QueryLog) censoredTarget() string {
|
func (c *QueryLog) censoredTarget() string {
|
||||||
|
|
|
@ -54,6 +54,7 @@ var _ = Describe("QueryLogConfig", func() {
|
||||||
|
|
||||||
Expect(hook.Calls).ShouldNot(BeEmpty())
|
Expect(hook.Calls).ShouldNot(BeEmpty())
|
||||||
Expect(hook.Messages).Should(ContainElement(ContainSubstring("logRetentionDays:")))
|
Expect(hook.Messages).Should(ContainElement(ContainSubstring("logRetentionDays:")))
|
||||||
|
Expect(hook.Messages).Should(ContainElement(ContainSubstring("sudn:")))
|
||||||
})
|
})
|
||||||
|
|
||||||
DescribeTable("secret censoring", func(target string) {
|
DescribeTable("secret censoring", func(target string) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package querylog
|
package querylog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/0xERR0R/blocky/log"
|
"github.com/0xERR0R/blocky/log"
|
||||||
|
@ -19,22 +20,36 @@ func NewLoggerWriter() *LoggerWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *LoggerWriter) Write(entry *LogEntry) {
|
func (d *LoggerWriter) Write(entry *LogEntry) {
|
||||||
d.logger.WithFields(
|
fields := LogEntryFields(entry)
|
||||||
logrus.Fields{
|
|
||||||
"client_ip": entry.ClientIP,
|
d.logger.WithFields(fields).Infof("query resolved")
|
||||||
"client_names": strings.Join(entry.ClientNames, "; "),
|
|
||||||
"response_reason": entry.ResponseReason,
|
|
||||||
"response_type": entry.ResponseType,
|
|
||||||
"response_code": entry.ResponseCode,
|
|
||||||
"question_name": entry.QuestionName,
|
|
||||||
"question_type": entry.QuestionType,
|
|
||||||
"answer": entry.Answer,
|
|
||||||
"duration_ms": entry.DurationMs,
|
|
||||||
"hostname": util.HostnameString(),
|
|
||||||
},
|
|
||||||
).Infof("query resolved")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *LoggerWriter) CleanUp() {
|
func (d *LoggerWriter) CleanUp() {
|
||||||
// Nothing to do
|
// Nothing to do
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LogEntryFields(entry *LogEntry) logrus.Fields {
|
||||||
|
return withoutZeroes(logrus.Fields{
|
||||||
|
"client_ip": entry.ClientIP,
|
||||||
|
"client_names": strings.Join(entry.ClientNames, "; "),
|
||||||
|
"response_reason": entry.ResponseReason,
|
||||||
|
"response_type": entry.ResponseType,
|
||||||
|
"response_code": entry.ResponseCode,
|
||||||
|
"question_name": entry.QuestionName,
|
||||||
|
"question_type": entry.QuestionType,
|
||||||
|
"answer": entry.Answer,
|
||||||
|
"duration_ms": entry.DurationMs,
|
||||||
|
"hostname": util.HostnameString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func withoutZeroes(fields logrus.Fields) logrus.Fields {
|
||||||
|
for k, v := range fields {
|
||||||
|
if reflect.ValueOf(v).IsZero() {
|
||||||
|
delete(fields, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package querylog
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/sirupsen/logrus/hooks/test"
|
"github.com/sirupsen/logrus/hooks/test"
|
||||||
|
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
|
@ -34,4 +35,50 @@ var _ = Describe("LoggerWriter", func() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Describe("LogEntryFields", func() {
|
||||||
|
It("should return log fields", func() {
|
||||||
|
entry := LogEntry{
|
||||||
|
ClientIP: "ip",
|
||||||
|
DurationMs: 100,
|
||||||
|
QuestionType: "qtype",
|
||||||
|
ResponseCode: "rcode",
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := LogEntryFields(&entry)
|
||||||
|
|
||||||
|
Expect(fields).Should(HaveKeyWithValue("client_ip", entry.ClientIP))
|
||||||
|
Expect(fields).Should(HaveKeyWithValue("duration_ms", entry.DurationMs))
|
||||||
|
Expect(fields).Should(HaveKeyWithValue("question_type", entry.QuestionType))
|
||||||
|
Expect(fields).Should(HaveKeyWithValue("response_code", entry.ResponseCode))
|
||||||
|
Expect(fields).Should(HaveKey("hostname"))
|
||||||
|
|
||||||
|
Expect(fields).ShouldNot(HaveKey("client_names"))
|
||||||
|
Expect(fields).ShouldNot(HaveKey("question_name"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
DescribeTable("withoutZeroes",
|
||||||
|
func(value any, isZero bool) {
|
||||||
|
fields := withoutZeroes(logrus.Fields{"a": value})
|
||||||
|
|
||||||
|
if isZero {
|
||||||
|
Expect(fields).Should(BeEmpty())
|
||||||
|
} else {
|
||||||
|
Expect(fields).ShouldNot(BeEmpty())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Entry("empty string",
|
||||||
|
"",
|
||||||
|
true),
|
||||||
|
Entry("non-empty string",
|
||||||
|
"something",
|
||||||
|
false),
|
||||||
|
Entry("zero int",
|
||||||
|
0,
|
||||||
|
true),
|
||||||
|
Entry("non-zero int",
|
||||||
|
1,
|
||||||
|
false),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -115,20 +115,40 @@ func (r *QueryLoggingResolver) Resolve(ctx context.Context, request *model.Reque
|
||||||
ctx, logger := r.log(ctx)
|
ctx, logger := r.log(ctx)
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
resp, err := r.next.Resolve(ctx, request)
|
resp, err := r.next.Resolve(ctx, request)
|
||||||
|
|
||||||
duration := time.Since(start).Milliseconds()
|
duration := time.Since(start).Milliseconds()
|
||||||
|
|
||||||
if err == nil {
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := r.createLogEntry(request, resp, start, duration)
|
||||||
|
|
||||||
|
if r.ignore(resp) {
|
||||||
|
// Log to the console for debugging purposes
|
||||||
|
logger.WithFields(querylog.LogEntryFields(entry)).Debug("ignored querylog entry")
|
||||||
|
} else {
|
||||||
select {
|
select {
|
||||||
case r.logChan <- r.createLogEntry(request, resp, start, duration):
|
case r.logChan <- entry:
|
||||||
default:
|
default:
|
||||||
logger.Error("query log writer is too slow, log entry will be dropped")
|
logger.Error("query log writer is too slow, log entry will be dropped")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp, err
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *QueryLoggingResolver) ignore(response *model.Response) bool {
|
||||||
|
cfg := r.cfg.Ignore
|
||||||
|
|
||||||
|
if cfg.SUDN && response.RType == model.ResponseTypeSPECIAL {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we add more ways to ignore entries, it would be nice to log why it's ignored in the debug log
|
||||||
|
// Probably make this func return a (string, bool).
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *QueryLoggingResolver) createLogEntry(request *model.Request, response *model.Response,
|
func (r *QueryLoggingResolver) createLogEntry(request *model.Request, response *model.Response,
|
||||||
|
|
|
@ -13,6 +13,8 @@ import (
|
||||||
. "github.com/0xERR0R/blocky/helpertest"
|
. "github.com/0xERR0R/blocky/helpertest"
|
||||||
"github.com/0xERR0R/blocky/log"
|
"github.com/0xERR0R/blocky/log"
|
||||||
"github.com/0xERR0R/blocky/querylog"
|
"github.com/0xERR0R/blocky/querylog"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
"github.com/0xERR0R/blocky/config"
|
"github.com/0xERR0R/blocky/config"
|
||||||
. "github.com/0xERR0R/blocky/model"
|
. "github.com/0xERR0R/blocky/model"
|
||||||
|
@ -21,7 +23,6 @@ import (
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SlowMockWriter struct {
|
type SlowMockWriter struct {
|
||||||
|
@ -43,6 +44,7 @@ var _ = Describe("QueryLoggingResolver", func() {
|
||||||
sutConfig config.QueryLog
|
sutConfig config.QueryLog
|
||||||
m *mockResolver
|
m *mockResolver
|
||||||
tmpDir *TmpFolder
|
tmpDir *TmpFolder
|
||||||
|
mockRType ResponseType
|
||||||
mockAnswer *dns.Msg
|
mockAnswer *dns.Msg
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
@ -59,6 +61,12 @@ var _ = Describe("QueryLoggingResolver", func() {
|
||||||
ctx, cancelFn = context.WithCancel(context.Background())
|
ctx, cancelFn = context.WithCancel(context.Background())
|
||||||
DeferCleanup(cancelFn)
|
DeferCleanup(cancelFn)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
sutConfig, err = config.WithDefaults[config.QueryLog]()
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
|
|
||||||
|
mockRType = ResponseTypeRESOLVED
|
||||||
mockAnswer = new(dns.Msg)
|
mockAnswer = new(dns.Msg)
|
||||||
tmpDir = NewTmpFolder("queryLoggingResolver")
|
tmpDir = NewTmpFolder("queryLoggingResolver")
|
||||||
})
|
})
|
||||||
|
@ -69,8 +77,15 @@ var _ = Describe("QueryLoggingResolver", func() {
|
||||||
}
|
}
|
||||||
|
|
||||||
sut = NewQueryLoggingResolver(ctx, sutConfig)
|
sut = NewQueryLoggingResolver(ctx, sutConfig)
|
||||||
m = &mockResolver{}
|
|
||||||
m.On("Resolve", mock.Anything).Return(&Response{Res: mockAnswer, Reason: "reason"}, nil)
|
m = &mockResolver{
|
||||||
|
ResolveFn: func(context.Context, *Request) (*Response, error) {
|
||||||
|
return &Response{RType: mockRType, Res: mockAnswer, Reason: "reason"}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.On("Resolve", mock.Anything).Return(autoAnswer, nil)
|
||||||
|
|
||||||
sut.Next(m)
|
sut.Next(m)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -109,6 +124,57 @@ var _ = Describe("QueryLoggingResolver", func() {
|
||||||
m.AssertExpectations(GinkgoT())
|
m.AssertExpectations(GinkgoT())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Describe("ignore", func() {
|
||||||
|
var ignored *log.MockLoggerHook
|
||||||
|
|
||||||
|
JustBeforeEach(func() {
|
||||||
|
// Stop background goroutines
|
||||||
|
cancelFn()
|
||||||
|
|
||||||
|
ctx, cancelFn = context.WithCancel(context.Background())
|
||||||
|
DeferCleanup(cancelFn)
|
||||||
|
|
||||||
|
// Capture written logs
|
||||||
|
sut.logChan = make(chan *querylog.LogEntry, 16)
|
||||||
|
|
||||||
|
// Capture ignored logs
|
||||||
|
{
|
||||||
|
var logger *logrus.Entry
|
||||||
|
|
||||||
|
logger, ignored = log.NewMockEntry()
|
||||||
|
ctx, _ = log.NewCtx(ctx, logger)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("SUDN", func() {
|
||||||
|
JustBeforeEach(func() {
|
||||||
|
sut.cfg.Ignore.SUDN = true
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should not log SUDN responses", func() {
|
||||||
|
mockRType = ResponseTypeSPECIAL
|
||||||
|
|
||||||
|
_, err := sut.Resolve(ctx, newRequestWithClient("example.com.", A, "192.168.178.25", "client1"))
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
|
|
||||||
|
Expect(sut.logChan).Should(BeEmpty())
|
||||||
|
Expect(ignored.Calls).Should(HaveLen(1))
|
||||||
|
Expect(ignored.Messages).Should(ContainElement(ContainSubstring("ignored querylog entry")))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should log other responses", func() {
|
||||||
|
mockRType = ResponseTypeBLOCKED
|
||||||
|
|
||||||
|
_, err := sut.Resolve(ctx, newRequestWithClient("example.com.", A, "192.168.178.25", "client1"))
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
|
|
||||||
|
Expect(sut.logChan).ShouldNot(BeEmpty())
|
||||||
|
Expect(ignored.Calls).Should(BeEmpty())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
When("Configuration with logging per client", func() {
|
When("Configuration with logging per client", func() {
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
sutConfig = config.QueryLog{
|
sutConfig = config.QueryLog{
|
||||||
|
|
Loading…
Reference in New Issue