mirror of https://github.com/0xERR0R/blocky.git
This commit is contained in:
parent
797d71d57c
commit
2b49c2048f
|
@ -86,6 +86,10 @@ type QueryLogType int16
|
|||
// )
|
||||
type StartStrategyType uint16
|
||||
|
||||
// QueryLogField data field to be logged
|
||||
// ENUM(clientIP,clientName,responseReason,responseAnswer,question,duration)
|
||||
type QueryLogField string
|
||||
|
||||
type QType dns.Type
|
||||
|
||||
func (c QType) String() string {
|
||||
|
@ -567,11 +571,12 @@ type CachingConfig struct {
|
|||
|
||||
// QueryLogConfig configuration for the query logging
|
||||
type QueryLogConfig struct {
|
||||
Target string `yaml:"target"`
|
||||
Type QueryLogType `yaml:"type"`
|
||||
LogRetentionDays uint64 `yaml:"logRetentionDays"`
|
||||
CreationAttempts int `yaml:"creationAttempts" default:"3"`
|
||||
CreationCooldown Duration `yaml:"creationCooldown" default:"2s"`
|
||||
Target string `yaml:"target"`
|
||||
Type QueryLogType `yaml:"type"`
|
||||
LogRetentionDays uint64 `yaml:"logRetentionDays"`
|
||||
CreationAttempts int `yaml:"creationAttempts" default:"3"`
|
||||
CreationCooldown Duration `yaml:"creationCooldown" default:"2s"`
|
||||
Fields []QueryLogField `yaml:"fields"`
|
||||
}
|
||||
|
||||
// RedisConfig configuration for the redis connection
|
||||
|
|
|
@ -157,6 +157,82 @@ func (x *NetProtocol) UnmarshalText(text []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
// QueryLogFieldClientIP is a QueryLogField of type clientIP.
|
||||
QueryLogFieldClientIP QueryLogField = "clientIP"
|
||||
// QueryLogFieldClientName is a QueryLogField of type clientName.
|
||||
QueryLogFieldClientName QueryLogField = "clientName"
|
||||
// QueryLogFieldResponseReason is a QueryLogField of type responseReason.
|
||||
QueryLogFieldResponseReason QueryLogField = "responseReason"
|
||||
// QueryLogFieldResponseAnswer is a QueryLogField of type responseAnswer.
|
||||
QueryLogFieldResponseAnswer QueryLogField = "responseAnswer"
|
||||
// QueryLogFieldQuestion is a QueryLogField of type question.
|
||||
QueryLogFieldQuestion QueryLogField = "question"
|
||||
// QueryLogFieldDuration is a QueryLogField of type duration.
|
||||
QueryLogFieldDuration QueryLogField = "duration"
|
||||
)
|
||||
|
||||
var ErrInvalidQueryLogField = fmt.Errorf("not a valid QueryLogField, try [%s]", strings.Join(_QueryLogFieldNames, ", "))
|
||||
|
||||
var _QueryLogFieldNames = []string{
|
||||
string(QueryLogFieldClientIP),
|
||||
string(QueryLogFieldClientName),
|
||||
string(QueryLogFieldResponseReason),
|
||||
string(QueryLogFieldResponseAnswer),
|
||||
string(QueryLogFieldQuestion),
|
||||
string(QueryLogFieldDuration),
|
||||
}
|
||||
|
||||
// QueryLogFieldNames returns a list of possible string values of QueryLogField.
|
||||
func QueryLogFieldNames() []string {
|
||||
tmp := make([]string, len(_QueryLogFieldNames))
|
||||
copy(tmp, _QueryLogFieldNames)
|
||||
return tmp
|
||||
}
|
||||
|
||||
// String implements the Stringer interface.
|
||||
func (x QueryLogField) String() string {
|
||||
return string(x)
|
||||
}
|
||||
|
||||
// String implements the Stringer interface.
|
||||
func (x QueryLogField) IsValid() bool {
|
||||
_, err := ParseQueryLogField(string(x))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
var _QueryLogFieldValue = map[string]QueryLogField{
|
||||
"clientIP": QueryLogFieldClientIP,
|
||||
"clientName": QueryLogFieldClientName,
|
||||
"responseReason": QueryLogFieldResponseReason,
|
||||
"responseAnswer": QueryLogFieldResponseAnswer,
|
||||
"question": QueryLogFieldQuestion,
|
||||
"duration": QueryLogFieldDuration,
|
||||
}
|
||||
|
||||
// ParseQueryLogField attempts to convert a string to a QueryLogField.
|
||||
func ParseQueryLogField(name string) (QueryLogField, error) {
|
||||
if x, ok := _QueryLogFieldValue[name]; ok {
|
||||
return x, nil
|
||||
}
|
||||
return QueryLogField(""), fmt.Errorf("%s is %w", name, ErrInvalidQueryLogField)
|
||||
}
|
||||
|
||||
// MarshalText implements the text marshaller method.
|
||||
func (x QueryLogField) MarshalText() ([]byte, error) {
|
||||
return []byte(string(x)), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the text unmarshaller method.
|
||||
func (x *QueryLogField) UnmarshalText(text []byte) error {
|
||||
tmp, err := ParseQueryLogField(string(text))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*x = tmp
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
// QueryLogTypeConsole is a QueryLogType of type Console.
|
||||
// use logger as fallback
|
||||
|
|
|
@ -177,6 +177,10 @@ queryLog:
|
|||
creationAttempts: 1
|
||||
# optional: Time between the creation attempts, default: 2s
|
||||
creationCooldown: 2s
|
||||
# optional: Which fields should be logged. You can choose one or more from: clientIP, clientName, responseReason, responseAnswer, question, duration. If not defined, it logs all fields
|
||||
fields:
|
||||
- clientIP
|
||||
- duration
|
||||
|
||||
# optional: Blocky can synchronize its cache and blocking state between multiple instances through redis.
|
||||
redis:
|
||||
|
|
|
@ -600,22 +600,37 @@ You can select one of following query log types:
|
|||
- `console` - log into console output
|
||||
- `none` - do not log any queries
|
||||
|
||||
### Query log fields
|
||||
|
||||
You can choose which information from processed DNS request and response should be logged in the target system. You can define one or more of following fields:
|
||||
|
||||
- `clientIP` - origin IP address from the request
|
||||
- `clientName` - resolved client name(s) from the origins request
|
||||
- `responseReason` - reason for the response (e.g. from which upstream resolver), response type and code
|
||||
- `responseAnswer` - returned DNS answer
|
||||
- `question` - DNS question from the request
|
||||
- `duration` - request processing time in milliseconds
|
||||
|
||||
!!! hint
|
||||
If not defined, blocky will log all available information
|
||||
|
||||
Configuration parameters:
|
||||
|
||||
| Parameter | Type | Mandatory | Default value | Description |
|
||||
|---------------------------|----------------------------------------------------------------------|-----------|---------------|------------------------------------------------------------------------------------|
|
||||
| queryLog.type | enum (mysql, postgresql, csv, csv-client, console, none (see above)) | no | | Type of logging target. Console if empty |
|
||||
| queryLog.target | string | no | | directory for writing the logs (for csv) or database url (for mysql or postgresql) |
|
||||
| queryLog.logRetentionDays | int | no | 0 | if > 0, deletes log files/database entries which are older than ... days |
|
||||
| queryLog.creationAttempts | int | no | 3 | Max attempts to create specific query log writer |
|
||||
| queryLog.CreationCooldown | duration format | no | 2 | Time between the creation attempts |
|
||||
| Parameter | Type | Mandatory | Default value | Description |
|
||||
|---------------------------|--------------------------------------------------------------------------------------|-----------|---------------|------------------------------------------------------------------------------------|
|
||||
| queryLog.type | enum (mysql, postgresql, csv, csv-client, console, none (see above)) | no | | Type of logging target. Console if empty |
|
||||
| queryLog.target | string | no | | directory for writing the logs (for csv) or database url (for mysql or postgresql) |
|
||||
| queryLog.logRetentionDays | int | no | 0 | if > 0, deletes log files/database entries which are older than ... days |
|
||||
| queryLog.creationAttempts | int | no | 3 | Max attempts to create specific query log writer |
|
||||
| queryLog.CreationCooldown | duration format | no | 2 | Time between the creation attempts |
|
||||
| queryLog.fields | list enum (clientIP, clientName, responseReason, responseAnswer, question, duration) | no | all | which information should be logged |
|
||||
|
||||
!!! hint
|
||||
|
||||
Please ensure, that the log directory is writable or database exists. If you use docker, please ensure, that the directory is properly
|
||||
mounted (e.g. volume)
|
||||
|
||||
example for CSV format
|
||||
example for CSV format with limited logging information
|
||||
!!! example
|
||||
|
||||
```yaml
|
||||
|
@ -623,6 +638,9 @@ example for CSV format
|
|||
type: csv
|
||||
target: /logs
|
||||
logRetentionDays: 7
|
||||
fields:
|
||||
- clientIP
|
||||
- duration
|
||||
```
|
||||
|
||||
example for Database
|
||||
|
|
1
go.mod
1
go.mod
|
@ -36,7 +36,6 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
|
||||
github.com/docker/go-connections v0.4.0
|
||||
github.com/dosgo/zigtool v0.0.0-20210923085854-9c6fc1d62198
|
||||
github.com/testcontainers/testcontainers-go v0.15.0
|
||||
|
|
1
go.sum
1
go.sum
|
@ -90,7 +90,6 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:H
|
|||
github.com/abice/go-enum v0.5.3 h1:Ghq0aWp+tCNZFAb4lFK7UnjzUJQTS1atIMjHkX+Gex4=
|
||||
github.com/abice/go-enum v0.5.3/go.mod h1:jf915DI7NxXZRwk8qDgZJKq2raAtwcPBXJRh9WVgtU0=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
|
|
|
@ -10,9 +10,8 @@ import (
|
|||
"gorm.io/gorm/logger"
|
||||
|
||||
"github.com/0xERR0R/blocky/log"
|
||||
|
||||
"github.com/0xERR0R/blocky/util"
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"golang.org/x/net/publicsuffix"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
|
@ -126,21 +125,21 @@ func (d *DatabaseWriter) periodicFlush() {
|
|||
}
|
||||
|
||||
func (d *DatabaseWriter) Write(entry *LogEntry) {
|
||||
domain := util.ExtractDomain(entry.Request.Req.Question[0])
|
||||
domain := util.ExtractDomainOnly(entry.QuestionName)
|
||||
eTLD, _ := publicsuffix.EffectiveTLDPlusOne(domain)
|
||||
|
||||
e := &logEntry{
|
||||
RequestTS: &entry.Start,
|
||||
ClientIP: entry.Request.ClientIP.String(),
|
||||
ClientName: strings.Join(entry.Request.ClientNames, "; "),
|
||||
ClientIP: entry.ClientIP,
|
||||
ClientName: strings.Join(entry.ClientNames, "; "),
|
||||
DurationMs: entry.DurationMs,
|
||||
Reason: entry.Response.Reason,
|
||||
ResponseType: entry.Response.RType.String(),
|
||||
QuestionType: dns.TypeToString[entry.Request.Req.Question[0].Qtype],
|
||||
Reason: entry.ResponseReason,
|
||||
ResponseType: entry.ResponseType,
|
||||
QuestionType: entry.QuestionType,
|
||||
QuestionName: domain,
|
||||
EffectiveTLDP: eTLD,
|
||||
Answer: util.AnswerToString(entry.Response.Res.Answer),
|
||||
ResponseCode: dns.RcodeToString[entry.Response.Res.Rcode],
|
||||
Answer: entry.Answer,
|
||||
ResponseCode: entry.ResponseCode,
|
||||
Hostname: util.HostnameString(),
|
||||
}
|
||||
|
||||
|
|
|
@ -3,11 +3,6 @@ package querylog
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/0xERR0R/blocky/log"
|
||||
"github.com/0xERR0R/blocky/model"
|
||||
"github.com/0xERR0R/blocky/util"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
|
@ -24,16 +19,10 @@ var _ = Describe("DatabaseWriter", func() {
|
|||
var (
|
||||
sqliteDB gorm.Dialector
|
||||
writer *DatabaseWriter
|
||||
request *model.Request
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
sqliteDB = sqlite.Open("file::memory:")
|
||||
|
||||
request = &model.Request{
|
||||
Req: util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA)),
|
||||
Log: logrus.NewEntry(log.Log()),
|
||||
}
|
||||
})
|
||||
|
||||
When("New log entry was created", func() {
|
||||
|
@ -43,27 +32,14 @@ var _ = Describe("DatabaseWriter", func() {
|
|||
})
|
||||
|
||||
It("should be persisted in the database", func() {
|
||||
res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122")
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
response := &model.Response{
|
||||
Res: res,
|
||||
Reason: "Resolved",
|
||||
RType: model.ResponseTypeRESOLVED,
|
||||
}
|
||||
|
||||
// one entry with now as timestamp
|
||||
writer.Write(&LogEntry{
|
||||
Request: request,
|
||||
Response: response,
|
||||
Start: time.Now(),
|
||||
DurationMs: 20,
|
||||
})
|
||||
|
||||
// one entry before 2 days
|
||||
writer.Write(&LogEntry{
|
||||
Request: request,
|
||||
Response: response,
|
||||
Start: time.Now().AddDate(0, 0, -2),
|
||||
DurationMs: 20,
|
||||
})
|
||||
|
@ -102,27 +78,14 @@ var _ = Describe("DatabaseWriter", func() {
|
|||
})
|
||||
|
||||
It("these old entries should be deleted", func() {
|
||||
res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122")
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
response := &model.Response{
|
||||
Res: res,
|
||||
Reason: "Resolved",
|
||||
RType: model.ResponseTypeRESOLVED,
|
||||
}
|
||||
|
||||
// one entry with now as timestamp
|
||||
writer.Write(&LogEntry{
|
||||
Request: request,
|
||||
Response: response,
|
||||
Start: time.Now(),
|
||||
DurationMs: 20,
|
||||
})
|
||||
|
||||
// one entry before 2 days -> should be deleted
|
||||
writer.Write(&LogEntry{
|
||||
Request: request,
|
||||
Response: response,
|
||||
Start: time.Now().AddDate(0, 0, -2),
|
||||
DurationMs: 20,
|
||||
})
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
|
||||
"github.com/0xERR0R/blocky/log"
|
||||
"github.com/0xERR0R/blocky/util"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
@ -44,7 +43,7 @@ func (d *FileWriter) Write(entry *LogEntry) {
|
|||
dateString := entry.Start.Format("2006-01-02")
|
||||
|
||||
if d.perClient {
|
||||
clientPrefix = strings.Join(entry.Request.ClientNames, "-")
|
||||
clientPrefix = strings.Join(entry.ClientNames, "-")
|
||||
} else {
|
||||
clientPrefix = "ALL"
|
||||
}
|
||||
|
@ -102,18 +101,17 @@ func (d *FileWriter) CleanUp() {
|
|||
}
|
||||
|
||||
func createQueryLogRow(logEntry *LogEntry) []string {
|
||||
request := logEntry.Request
|
||||
response := logEntry.Response
|
||||
|
||||
return []string{
|
||||
logEntry.Start.Format("2006-01-02 15:04:05"),
|
||||
request.ClientIP.String(),
|
||||
strings.Join(request.ClientNames, "; "),
|
||||
logEntry.ClientIP,
|
||||
strings.Join(logEntry.ClientNames, "; "),
|
||||
fmt.Sprintf("%d", logEntry.DurationMs),
|
||||
response.Reason,
|
||||
util.QuestionToString(request.Req.Question),
|
||||
util.AnswerToString(response.Res.Answer),
|
||||
dns.RcodeToString[response.Res.Rcode],
|
||||
logEntry.ResponseReason,
|
||||
logEntry.QuestionName,
|
||||
logEntry.Answer,
|
||||
logEntry.ResponseCode,
|
||||
logEntry.ResponseType,
|
||||
logEntry.QuestionType,
|
||||
util.HostnameString(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,6 @@ import (
|
|||
"github.com/0xERR0R/blocky/helpertest"
|
||||
"github.com/0xERR0R/blocky/log"
|
||||
|
||||
"github.com/0xERR0R/blocky/model"
|
||||
"github.com/0xERR0R/blocky/util"
|
||||
"github.com/miekg/dns"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
@ -45,41 +42,19 @@ var _ = Describe("FileWriter", func() {
|
|||
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122")
|
||||
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
By("entry for client 1", func() {
|
||||
writer.Write(&LogEntry{
|
||||
Request: &model.Request{
|
||||
ClientNames: []string{"client1"},
|
||||
Req: util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA)),
|
||||
RequestTS: time.Time{},
|
||||
},
|
||||
Response: &model.Response{
|
||||
Res: res,
|
||||
Reason: "Resolved",
|
||||
RType: model.ResponseTypeRESOLVED,
|
||||
},
|
||||
Start: time.Now(),
|
||||
DurationMs: 20,
|
||||
ClientNames: []string{"client1"},
|
||||
Start: time.Now(),
|
||||
DurationMs: 20,
|
||||
})
|
||||
})
|
||||
|
||||
By("entry for client 2", func() {
|
||||
writer.Write(&LogEntry{
|
||||
Request: &model.Request{
|
||||
ClientNames: []string{"client2"},
|
||||
Req: util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA)),
|
||||
RequestTS: time.Time{},
|
||||
},
|
||||
Response: &model.Response{
|
||||
Res: res,
|
||||
Reason: "Resolved",
|
||||
RType: model.ResponseTypeRESOLVED,
|
||||
},
|
||||
Start: time.Now(),
|
||||
DurationMs: 20,
|
||||
ClientNames: []string{"client2"},
|
||||
Start: time.Now(),
|
||||
DurationMs: 20,
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -94,41 +69,19 @@ var _ = Describe("FileWriter", func() {
|
|||
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122")
|
||||
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
By("entry for client 1", func() {
|
||||
writer.Write(&LogEntry{
|
||||
Request: &model.Request{
|
||||
ClientNames: []string{"client1"},
|
||||
Req: util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA)),
|
||||
RequestTS: time.Time{},
|
||||
},
|
||||
Response: &model.Response{
|
||||
Res: res,
|
||||
Reason: "Resolved",
|
||||
RType: model.ResponseTypeRESOLVED,
|
||||
},
|
||||
Start: time.Now(),
|
||||
DurationMs: 20,
|
||||
ClientNames: []string{"client1"},
|
||||
Start: time.Now(),
|
||||
DurationMs: 20,
|
||||
})
|
||||
})
|
||||
|
||||
By("entry for client 2", func() {
|
||||
writer.Write(&LogEntry{
|
||||
Request: &model.Request{
|
||||
ClientNames: []string{"client2"},
|
||||
Req: util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA)),
|
||||
RequestTS: time.Time{},
|
||||
},
|
||||
Response: &model.Response{
|
||||
Res: res,
|
||||
Reason: "Resolved",
|
||||
RType: model.ResponseTypeRESOLVED,
|
||||
},
|
||||
Start: time.Now(),
|
||||
DurationMs: 20,
|
||||
ClientNames: []string{"client2"},
|
||||
Start: time.Now(),
|
||||
DurationMs: 20,
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -150,40 +103,18 @@ var _ = Describe("FileWriter", func() {
|
|||
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122")
|
||||
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
By("entry today", func() {
|
||||
writer.Write(&LogEntry{
|
||||
Request: &model.Request{
|
||||
ClientNames: []string{"client1"},
|
||||
Req: util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA)),
|
||||
RequestTS: time.Now(),
|
||||
},
|
||||
Response: &model.Response{
|
||||
Res: res,
|
||||
Reason: "Resolved",
|
||||
RType: model.ResponseTypeRESOLVED,
|
||||
},
|
||||
Start: time.Now(),
|
||||
DurationMs: 20,
|
||||
ClientNames: []string{"client1"},
|
||||
Start: time.Now(),
|
||||
DurationMs: 20,
|
||||
})
|
||||
})
|
||||
By("entry 2 days ago", func() {
|
||||
writer.Write(&LogEntry{
|
||||
Request: &model.Request{
|
||||
ClientNames: []string{"client1"},
|
||||
Req: util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA)),
|
||||
RequestTS: time.Now(),
|
||||
},
|
||||
Response: &model.Response{
|
||||
Res: res,
|
||||
Reason: "Resolved",
|
||||
RType: model.ResponseTypeRESOLVED,
|
||||
},
|
||||
Start: time.Now().AddDate(0, 0, -3),
|
||||
DurationMs: 20,
|
||||
ClientNames: []string{"client1"},
|
||||
Start: time.Now().AddDate(0, 0, -3),
|
||||
DurationMs: 20,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
|
||||
"github.com/0xERR0R/blocky/log"
|
||||
"github.com/0xERR0R/blocky/util"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
@ -22,12 +21,14 @@ func NewLoggerWriter() *LoggerWriter {
|
|||
func (d *LoggerWriter) Write(entry *LogEntry) {
|
||||
d.logger.WithFields(
|
||||
logrus.Fields{
|
||||
"client_ip": entry.Request.ClientIP,
|
||||
"client_names": strings.Join(entry.Request.ClientNames, "; "),
|
||||
"response_reason": entry.Response.Reason,
|
||||
"question": util.QuestionToString(entry.Request.Req.Question),
|
||||
"response_code": dns.RcodeToString[entry.Response.Res.Rcode],
|
||||
"answer": util.AnswerToString(entry.Response.Res.Answer),
|
||||
"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(),
|
||||
},
|
||||
|
|
|
@ -5,12 +5,7 @@ import (
|
|||
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
|
||||
"github.com/0xERR0R/blocky/log"
|
||||
"github.com/0xERR0R/blocky/model"
|
||||
"github.com/0xERR0R/blocky/util"
|
||||
"github.com/miekg/dns"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
)
|
||||
|
@ -23,21 +18,8 @@ var _ = Describe("LoggerWriter", func() {
|
|||
writer := NewLoggerWriter()
|
||||
logger, hook := test.NewNullLogger()
|
||||
writer.logger = logger.WithField("k", "v")
|
||||
request := &model.Request{
|
||||
Req: util.NewMsgWithQuestion("google.de.", dns.Type(dns.TypeA)),
|
||||
Log: logrus.NewEntry(log.Log()),
|
||||
}
|
||||
res, err := util.NewMsgWithAnswer("example.com", 123, dns.Type(dns.TypeA), "123.124.122.122")
|
||||
|
||||
Expect(err).Should(Succeed())
|
||||
response := &model.Response{
|
||||
Res: res,
|
||||
Reason: "Resolved",
|
||||
RType: model.ResponseTypeRESOLVED,
|
||||
}
|
||||
writer.Write(&LogEntry{
|
||||
Request: request,
|
||||
Response: response,
|
||||
Start: time.Now(),
|
||||
DurationMs: 20,
|
||||
})
|
||||
|
|
|
@ -2,15 +2,19 @@ package querylog
|
|||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/0xERR0R/blocky/model"
|
||||
)
|
||||
|
||||
type LogEntry struct {
|
||||
Request *model.Request
|
||||
Response *model.Response
|
||||
Start time.Time
|
||||
DurationMs int64
|
||||
Start time.Time
|
||||
ClientIP string
|
||||
ClientNames []string
|
||||
DurationMs int64
|
||||
ResponseReason string
|
||||
ResponseType string
|
||||
ResponseCode string
|
||||
QuestionType string
|
||||
QuestionName string
|
||||
Answer string
|
||||
}
|
||||
|
||||
type Writer interface {
|
||||
|
|
|
@ -7,7 +7,9 @@ import (
|
|||
"github.com/0xERR0R/blocky/config"
|
||||
"github.com/0xERR0R/blocky/model"
|
||||
"github.com/0xERR0R/blocky/querylog"
|
||||
"github.com/0xERR0R/blocky/util"
|
||||
"github.com/avast/retry-go/v4"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -25,6 +27,7 @@ type QueryLoggingResolver struct {
|
|||
logChan chan *querylog.LogEntry
|
||||
writer querylog.Writer
|
||||
logType config.QueryLogType
|
||||
fields []config.QueryLogField
|
||||
}
|
||||
|
||||
// NewQueryLoggingResolver returns a new resolver instance
|
||||
|
@ -74,6 +77,7 @@ func NewQueryLoggingResolver(cfg config.QueryLogConfig) ChainedResolver {
|
|||
logChan: logChan,
|
||||
writer: writer,
|
||||
logType: logType,
|
||||
fields: resolveQueryLogFields(cfg),
|
||||
}
|
||||
|
||||
go resolver.writeLog()
|
||||
|
@ -85,6 +89,24 @@ func NewQueryLoggingResolver(cfg config.QueryLogConfig) ChainedResolver {
|
|||
return &resolver
|
||||
}
|
||||
|
||||
func resolveQueryLogFields(cfg config.QueryLogConfig) []config.QueryLogField {
|
||||
var fields []config.QueryLogField
|
||||
|
||||
if len(cfg.Fields) == 0 {
|
||||
// no fields defined, use all fields as fallback
|
||||
for _, v := range config.QueryLogFieldNames() {
|
||||
qlt, err := config.ParseQueryLogField(v)
|
||||
util.LogOnError("ignoring unknown query log field", err)
|
||||
|
||||
fields = append(fields, qlt)
|
||||
}
|
||||
} else {
|
||||
fields = cfg.Fields
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
// triggers periodically cleanup of old log files
|
||||
func (r *QueryLoggingResolver) periodicCleanUp() {
|
||||
ticker := time.NewTicker(cleanUpRunPeriod)
|
||||
|
@ -112,11 +134,7 @@ func (r *QueryLoggingResolver) Resolve(request *model.Request) (*model.Response,
|
|||
|
||||
if err == nil {
|
||||
select {
|
||||
case r.logChan <- &querylog.LogEntry{
|
||||
Request: request,
|
||||
Response: resp,
|
||||
Start: start,
|
||||
DurationMs: duration}:
|
||||
case r.logChan <- r.createLogEntry(request, resp, start, duration):
|
||||
default:
|
||||
logger.Error("query log writer is too slow, log entry will be dropped")
|
||||
}
|
||||
|
@ -125,6 +143,42 @@ func (r *QueryLoggingResolver) Resolve(request *model.Request) (*model.Response,
|
|||
return resp, err
|
||||
}
|
||||
|
||||
func (r *QueryLoggingResolver) createLogEntry(request *model.Request, response *model.Response,
|
||||
start time.Time, durationMs int64) *querylog.LogEntry {
|
||||
entry := querylog.LogEntry{
|
||||
Start: start,
|
||||
ClientIP: "0.0.0.0",
|
||||
ClientNames: []string{"none"},
|
||||
}
|
||||
|
||||
for _, f := range r.fields {
|
||||
switch f {
|
||||
case config.QueryLogFieldClientIP:
|
||||
entry.ClientIP = request.ClientIP.String()
|
||||
|
||||
case config.QueryLogFieldClientName:
|
||||
entry.ClientNames = request.ClientNames
|
||||
|
||||
case config.QueryLogFieldResponseReason:
|
||||
entry.ResponseReason = response.Reason
|
||||
entry.ResponseType = response.RType.String()
|
||||
entry.ResponseCode = dns.RcodeToString[response.Res.Rcode]
|
||||
|
||||
case config.QueryLogFieldResponseAnswer:
|
||||
entry.Answer = util.AnswerToString(response.Res.Answer)
|
||||
|
||||
case config.QueryLogFieldQuestion:
|
||||
entry.QuestionName = request.Req.Question[0].Name
|
||||
entry.QuestionType = dns.TypeToString[request.Req.Question[0].Qtype]
|
||||
|
||||
case config.QueryLogFieldDuration:
|
||||
entry.DurationMs = durationMs
|
||||
}
|
||||
}
|
||||
|
||||
return &entry
|
||||
}
|
||||
|
||||
// write entry: if log directory is configured, write to log file
|
||||
func (r *QueryLoggingResolver) writeLog() {
|
||||
for logEntry := range r.logChan {
|
||||
|
@ -147,6 +201,7 @@ func (r *QueryLoggingResolver) Configuration() (result []string) {
|
|||
result = append(result, fmt.Sprintf("type: \"%s\"", r.logType))
|
||||
result = append(result, fmt.Sprintf("target: \"%s\"", r.target))
|
||||
result = append(result, fmt.Sprintf("logRetentionDays: %d", r.logRetentionDays))
|
||||
result = append(result, fmt.Sprintf("fields: %s", r.fields))
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
@ -110,8 +110,11 @@ var _ = Describe("QueryLoggingResolver", func() {
|
|||
g.Expect(csvLines[0][1]).Should(Equal("192.168.178.25"))
|
||||
g.Expect(csvLines[0][2]).Should(Equal("client1"))
|
||||
g.Expect(csvLines[0][4]).Should(Equal("reason"))
|
||||
g.Expect(csvLines[0][5]).Should(Equal("A (example.com.)"))
|
||||
g.Expect(csvLines[0][5]).Should(Equal("example.com."))
|
||||
g.Expect(csvLines[0][6]).Should(Equal("A (123.122.121.120)"))
|
||||
g.Expect(csvLines[0][7]).Should(Equal("NOERROR"))
|
||||
g.Expect(csvLines[0][8]).Should(Equal("RESOLVED"))
|
||||
g.Expect(csvLines[0][9]).Should(Equal("A"))
|
||||
}, "1s").Should(Succeed())
|
||||
|
||||
})
|
||||
|
@ -126,8 +129,11 @@ var _ = Describe("QueryLoggingResolver", func() {
|
|||
g.Expect(csvLines[0][1]).Should(Equal("192.168.178.26"))
|
||||
g.Expect(csvLines[0][2]).Should(Equal("cl/ient2\\$%&test"))
|
||||
g.Expect(csvLines[0][4]).Should(Equal("reason"))
|
||||
g.Expect(csvLines[0][5]).Should(Equal("A (example.com.)"))
|
||||
g.Expect(csvLines[0][5]).Should(Equal("example.com."))
|
||||
g.Expect(csvLines[0][6]).Should(Equal("A (123.122.121.120)"))
|
||||
g.Expect(csvLines[0][7]).Should(Equal("NOERROR"))
|
||||
g.Expect(csvLines[0][8]).Should(Equal("RESOLVED"))
|
||||
g.Expect(csvLines[0][9]).Should(Equal("A"))
|
||||
}, "1s").Should(Succeed())
|
||||
})
|
||||
})
|
||||
|
@ -165,20 +171,69 @@ var _ = Describe("QueryLoggingResolver", func() {
|
|||
g.Expect(csvLines[0][1]).Should(Equal("192.168.178.25"))
|
||||
g.Expect(csvLines[0][2]).Should(Equal("client1"))
|
||||
g.Expect(csvLines[0][4]).Should(Equal("reason"))
|
||||
g.Expect(csvLines[0][5]).Should(Equal("A (example.com.)"))
|
||||
g.Expect(csvLines[0][5]).Should(Equal("example.com."))
|
||||
g.Expect(csvLines[0][6]).Should(Equal("A (123.122.121.120)"))
|
||||
g.Expect(csvLines[0][7]).Should(Equal("NOERROR"))
|
||||
g.Expect(csvLines[0][8]).Should(Equal("RESOLVED"))
|
||||
g.Expect(csvLines[0][9]).Should(Equal("A"))
|
||||
|
||||
// client2 -> second line
|
||||
g.Expect(csvLines[1][1]).Should(Equal("192.168.178.26"))
|
||||
g.Expect(csvLines[1][2]).Should(Equal("client2"))
|
||||
g.Expect(csvLines[1][4]).Should(Equal("reason"))
|
||||
g.Expect(csvLines[1][5]).Should(Equal("A (example.com.)"))
|
||||
g.Expect(csvLines[1][5]).Should(Equal("example.com."))
|
||||
g.Expect(csvLines[1][6]).Should(Equal("A (123.122.121.120)"))
|
||||
g.Expect(csvLines[1][7]).Should(Equal("NOERROR"))
|
||||
g.Expect(csvLines[1][8]).Should(Equal("RESOLVED"))
|
||||
g.Expect(csvLines[1][9]).Should(Equal("A"))
|
||||
}, "1s").Should(Succeed())
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
When("Configuration with specific fields to log", func() {
|
||||
BeforeEach(func() {
|
||||
sutConfig = config.QueryLogConfig{
|
||||
Target: tmpDir.Path,
|
||||
Type: config.QueryLogTypeCsv,
|
||||
CreationAttempts: 1,
|
||||
CreationCooldown: config.Duration(time.Millisecond),
|
||||
Fields: []config.QueryLogField{config.QueryLogFieldClientIP},
|
||||
}
|
||||
mockAnswer, _ = util.NewMsgWithAnswer("example.com.", 300, dns.Type(dns.TypeA), "123.122.121.120")
|
||||
})
|
||||
It("should create one log file", func() {
|
||||
By("request from client 1", func() {
|
||||
resp, err = sut.Resolve(newRequestWithClient("example.com.", dns.Type(dns.TypeA), "192.168.178.25", "client1"))
|
||||
Expect(err).Should(Succeed())
|
||||
})
|
||||
|
||||
m.AssertExpectations(GinkgoT())
|
||||
|
||||
By("check log", func() {
|
||||
Eventually(func(g Gomega) {
|
||||
csvLines, err := readCsv(tmpDir.JoinPath(
|
||||
fmt.Sprintf("%s_ALL.log", time.Now().Format("2006-01-02"))))
|
||||
|
||||
g.Expect(err).Should(Succeed())
|
||||
g.Expect(csvLines).Should(HaveLen(1))
|
||||
|
||||
// ip will be logged
|
||||
g.Expect(csvLines[0][1]).Should(Equal("192.168.178.25"))
|
||||
g.Expect(csvLines[0][2]).Should(Equal("none"))
|
||||
g.Expect(csvLines[0][3]).Should(Equal("0"))
|
||||
g.Expect(csvLines[0][4]).Should(Equal(""))
|
||||
g.Expect(csvLines[0][5]).Should(Equal(""))
|
||||
g.Expect(csvLines[0][6]).Should(Equal(""))
|
||||
g.Expect(csvLines[0][7]).Should(Equal(""))
|
||||
g.Expect(csvLines[0][8]).Should(Equal(""))
|
||||
g.Expect(csvLines[0][9]).Should(Equal(""))
|
||||
}, "1s").Should(Succeed())
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
Describe("Slow writer", func() {
|
||||
|
|
Loading…
Reference in New Issue