Merge: + new query logs API

* commit '59c4a2886a97143e3a36912ec895dc1a06be88cc':
  openapi
  If there are no more older entries, `"oldest":""` is returned.
  fix search by "whitelisted", "rewritten"
  doc
  fix whois test
  + "dot"
  openapi
  * minor
  + client_proto
  * openapi
  + new query logs API
This commit is contained in:
Andrey Meshkov 2020-06-18 00:23:52 +03:00
commit b1c951fb2c
13 changed files with 116 additions and 80 deletions

View File

@ -1198,8 +1198,9 @@ When a new DNS request is received and processed, we store information about thi
"QH":"...", // target host name without the last dot "QH":"...", // target host name without the last dot
"QT":"...", // question type "QT":"...", // question type
"QC":"...", // question class "QC":"...", // question class
"Answer":"...", "CP":"" | "doh", // client connection protocol
"OrigAnswer":"...", "Answer":"base64 data",
"OrigAnswer":"base64 data",
"Result":{ "Result":{
"IsFiltered":true, "IsFiltered":true,
"Reason":3, "Reason":3,
@ -1232,16 +1233,28 @@ Request:
GET /control/querylog GET /control/querylog
?older_than=2006-01-02T15:04:05.999999999Z07:00 ?older_than=2006-01-02T15:04:05.999999999Z07:00
&filter_domain=... &search=...
&filter_client=... &response_status="..."
&filter_question_type=A | AAAA
&filter_response_status= | filtered
`older_than` setting is used for paging. UI uses an empty value for `older_than` on the first request and gets the latest log entries. To get the older entries, UI sets `older_than` to the `oldest` value from the server's response. `older_than` setting is used for paging. UI uses an empty value for `older_than` on the first request and gets the latest log entries. To get the older entries, UI sets `older_than` to the `oldest` value from the server's response.
If "filter" settings are set, server returns only entries that match the specified request. If search settings are set, server returns only entries that match the specified request.
For `filter.domain` and `filter.client` the server matches substrings by default: `adguard.com` matches `www.adguard.com`. Strict matching can be enabled by enclosing the value in double quotes: `"adguard.com"` matches `adguard.com` but doesn't match `www.adguard.com`. `search`:
match by domain name or client IP address.
The server matches substrings by default: e.g. `adguard.com` matches `www.adguard.com`.
Strict matching can be enabled by enclosing the value in double quotes: e.g. `"adguard.com"` matches `adguard.com` but doesn't match `www.adguard.com`.
`response_status`:
* all
* filtered - all kinds of filtering
* blocked - blocked or blocked service
* blocked_safebrowsing - blocked by safebrowsing
* blocked_parental - blocked by parental control
* whitelisted - whitelisted
* rewritten - all kinds of rewrites
* safe_search - enforced safe search
* processed - not blocked, not white-listed entries
Response: Response:
@ -1264,8 +1277,10 @@ Response:
} }
... ...
], ],
"upstream":"...", // Upstream URL starting with tcp://, tls://, https://, or with an IP address
"answer_dnssec": true, "answer_dnssec": true,
"client":"127.0.0.1", "client":"127.0.0.1",
"client_proto": "" (plain) | "doh" | "dot",
"elapsedMs":"0.098403", "elapsedMs":"0.098403",
"filterId":1, "filterId":1,
"question":{ "question":{
@ -1285,6 +1300,8 @@ Response:
The most recent entries are at the top of list. The most recent entries are at the top of list.
If there are no more older entries, `"oldest":""` is returned.
### API: Set querylog parameters ### API: Set querylog parameters

View File

@ -400,6 +400,11 @@ func checkDNS(input string, bootstrap []string) error {
return nil return nil
} }
// Control flow:
// web
// -> dnsforward.handleDOH -> dnsforward.ServeHTTP
// -> proxy.ServeHTTP -> proxy.handleDNSRequest
// -> dnsforward.handleDNSRequest
func (s *Server) handleDOH(w http.ResponseWriter, r *http.Request) { func (s *Server) handleDOH(w http.ResponseWriter, r *http.Request) {
if !s.conf.TLSAllowUnencryptedDOH && r.TLS == nil { if !s.conf.TLSAllowUnencryptedDOH && r.TLS == nil {
httpError(r, w, http.StatusNotFound, "Not Found") httpError(r, w, http.StatusNotFound, "Not Found")

View File

@ -39,6 +39,13 @@ func processQueryLogsAndStats(ctx *dnsContext) int {
Elapsed: elapsed, Elapsed: elapsed,
ClientIP: getIP(d.Addr), ClientIP: getIP(d.Addr),
} }
if d.Proto == "https" {
p.ClientProto = "doh"
} else if d.Proto == "tls" {
p.ClientProto = "dot"
}
if d.Upstream != nil { if d.Upstream != nil {
p.Upstream = d.Upstream.Address() p.Upstream = d.Upstream.Address()
} }

View File

@ -27,9 +27,9 @@ func TestWhois(t *testing.T) {
w := Whois{timeoutMsec: 5000} w := Whois{timeoutMsec: 5000}
resp, err := w.queryAll("8.8.8.8") resp, err := w.queryAll("8.8.8.8")
assert.True(t, err == nil) assert.Nil(t, err)
m := whoisParse(resp) m := whoisParse(resp)
assert.True(t, m["orgname"] == "Google LLC") assert.Equal(t, "Google LLC", m["orgname"])
assert.True(t, m["country"] == "US") assert.Equal(t, "US", m["country"])
assert.True(t, m["city"] == "Mountain View") assert.Equal(t, "Mountain View", m["city"])
} }

View File

@ -163,29 +163,26 @@ paths:
description: Limit the number of records to be returned description: Limit the number of records to be returned
schema: schema:
type: integer type: integer
- name: filter_domain - name: search
in: query in: query
description: Filter by domain name description: Filter by domain name or client IP
schema: schema:
type: string type: string
- name: filter_client - name: response_status
in: query
description: Filter by client
schema:
type: string
- name: filter_question_type
in: query
description: Filter by question type
schema:
type: string
- name: filter_response_status
in: query in: query
description: Filter by response status description: Filter by response status
schema: schema:
type: string type: string
enum: enum:
- null - all
- filtered - filtered
- blocked
- blocked_safebrowsing
- blocked_parental
- whitelisted
- rewritten
- safe_search
- processed
responses: responses:
"200": "200":
description: OK description: OK
@ -1420,11 +1417,19 @@ components:
description: Answer from upstream server (optional) description: Answer from upstream server (optional)
items: items:
$ref: "#/components/schemas/DnsAnswer" $ref: "#/components/schemas/DnsAnswer"
upstream:
type: string
description: Upstream URL starting with tcp://, tls://, https://, or with an IP address
answer_dnssec: answer_dnssec:
type: boolean type: boolean
client: client:
type: string type: string
example: 192.168.0.1 example: 192.168.0.1
client_proto:
enum:
- dot
- doh
- ""
elapsedMs: elapsedMs:
type: string type: string
example: "54.023928" example: "54.023928"

View File

@ -37,6 +37,9 @@ func decodeLogEntry(ent *logEntry, str string) {
case "QC": case "QC":
ent.QClass = v ent.QClass = v
case "CP":
ent.ClientProto = v
case "Answer": case "Answer":
ent.Answer, err = base64.StdEncoding.DecodeString(v) ent.Answer, err = base64.StdEncoding.DecodeString(v)
case "OrigAnswer": case "OrigAnswer":

View File

@ -67,6 +67,7 @@ func (l *queryLog) logEntryToJSONEntry(entry *logEntry) map[string]interface{} {
"elapsedMs": strconv.FormatFloat(entry.Elapsed.Seconds()*1000, 'f', -1, 64), "elapsedMs": strconv.FormatFloat(entry.Elapsed.Seconds()*1000, 'f', -1, 64),
"time": entry.Time.Format(time.RFC3339Nano), "time": entry.Time.Format(time.RFC3339Nano),
"client": l.getClientIP(entry.IP), "client": l.getClientIP(entry.IP),
"client_proto": entry.ClientProto,
} }
jsonEntry["question"] = map[string]interface{}{ jsonEntry["question"] = map[string]interface{}{
"host": entry.QHost, "host": entry.QHost,
@ -112,6 +113,8 @@ func (l *queryLog) logEntryToJSONEntry(entry *logEntry) map[string]interface{} {
} }
} }
jsonEntry["upstream"] = entry.Upstream
return jsonEntry return jsonEntry
} }

View File

@ -38,6 +38,8 @@ type logEntry struct {
QType string `json:"QT"` QType string `json:"QT"`
QClass string `json:"QC"` QClass string `json:"QC"`
ClientProto string `json:"CP"` // "" or "doh"
Answer []byte `json:",omitempty"` // sometimes empty answers happen like binerdunt.top or rev2.globalrootservers.net Answer []byte `json:",omitempty"` // sometimes empty answers happen like binerdunt.top or rev2.globalrootservers.net
OrigAnswer []byte `json:",omitempty"` OrigAnswer []byte `json:",omitempty"`
@ -122,6 +124,7 @@ func (l *queryLog) Add(params AddParams) {
Result: *params.Result, Result: *params.Result,
Elapsed: params.Elapsed, Elapsed: params.Elapsed,
Upstream: params.Upstream, Upstream: params.Upstream,
ClientProto: params.ClientProto,
} }
q := params.Question.Question[0] q := params.Question.Question[0]
entry.QHost = strings.ToLower(q.Name[:len(q.Name)-1]) // remove the last dot entry.QHost = strings.ToLower(q.Name[:len(q.Name)-1]) // remove the last dot

View File

@ -142,10 +142,6 @@ func (l *queryLog) parseSearchCriteria(q url.Values, name string, ct criteriaTyp
c.strict = true c.strict = true
} }
if ct == ctClient && l.conf.AnonymizeClientIP {
c.value = l.getClientIP(c.value)
}
if ct == ctFilteringStatus && !util.ContainsString(filteringStatusValues, c.value) { if ct == ctFilteringStatus && !util.ContainsString(filteringStatusValues, c.value) {
return false, c, fmt.Errorf("invalid value %s", c.value) return false, c, fmt.Errorf("invalid value %s", c.value)
} }
@ -180,10 +176,8 @@ func (l *queryLog) parseSearchParams(r *http.Request) (*searchParams, error) {
} }
paramNames := map[string]criteriaType{ paramNames := map[string]criteriaType{
"filter_domain": ctDomain, "search": ctDomainOrClient,
"filter_client": ctClient, "response_status": ctFilteringStatus,
"filter_question_type": ctQuestionType,
"filter_response_status": ctFilteringStatus,
} }
for k, v := range paramNames { for k, v := range paramNames {

View File

@ -57,7 +57,7 @@ func TestQueryLog(t *testing.T) {
// search by domain (strict) // search by domain (strict)
params = newSearchParams() params = newSearchParams()
params.searchCriteria = append(params.searchCriteria, searchCriteria{ params.searchCriteria = append(params.searchCriteria, searchCriteria{
criteriaType: ctDomain, criteriaType: ctDomainOrClient,
strict: true, strict: true,
value: "test.example.org", value: "test.example.org",
}) })
@ -68,7 +68,7 @@ func TestQueryLog(t *testing.T) {
// search by domain (not strict) // search by domain (not strict)
params = newSearchParams() params = newSearchParams()
params.searchCriteria = append(params.searchCriteria, searchCriteria{ params.searchCriteria = append(params.searchCriteria, searchCriteria{
criteriaType: ctDomain, criteriaType: ctDomainOrClient,
strict: false, strict: false,
value: "example.org", value: "example.org",
}) })
@ -81,7 +81,7 @@ func TestQueryLog(t *testing.T) {
// search by client IP (strict) // search by client IP (strict)
params = newSearchParams() params = newSearchParams()
params.searchCriteria = append(params.searchCriteria, searchCriteria{ params.searchCriteria = append(params.searchCriteria, searchCriteria{
criteriaType: ctClient, criteriaType: ctDomainOrClient,
strict: true, strict: true,
value: "2.2.2.2", value: "2.2.2.2",
}) })
@ -92,7 +92,7 @@ func TestQueryLog(t *testing.T) {
// search by client IP (part of) // search by client IP (part of)
params = newSearchParams() params = newSearchParams()
params.searchCriteria = append(params.searchCriteria, searchCriteria{ params.searchCriteria = append(params.searchCriteria, searchCriteria{
criteriaType: ctClient, criteriaType: ctDomainOrClient,
strict: false, strict: false,
value: "2.2.2", value: "2.2.2",
}) })

View File

@ -47,7 +47,8 @@ type AddParams struct {
Result *dnsfilter.Result // Filtering result (optional) Result *dnsfilter.Result // Filtering result (optional)
Elapsed time.Duration // Time spent for processing the request Elapsed time.Duration // Time spent for processing the request
ClientIP net.IP ClientIP net.IP
Upstream string Upstream string // Upstream server URL
ClientProto string // Protocol for the client connection: "" (plain), "doh", "dot"
} }
// New - create a new instance of the query log // New - create a new instance of the query log

View File

@ -129,7 +129,9 @@ func (l *queryLog) searchFiles(params *searchParams) ([]*logEntry, time.Time, in
} }
} }
if oldestNano != 0 {
oldest = time.Unix(0, oldestNano) oldest = time.Unix(0, oldestNano)
}
return entries, oldest, total return entries, oldest, total
} }

View File

@ -9,9 +9,7 @@ import (
type criteriaType int type criteriaType int
const ( const (
ctDomain criteriaType = iota // domain name ctDomainOrClient criteriaType = iota // domain name or client IP address
ctClient // client IP address
ctQuestionType // question type
ctFilteringStatus // filtering status ctFilteringStatus // filtering status
) )
@ -25,6 +23,7 @@ const (
filteringStatusWhitelisted = "whitelisted" // whitelisted filteringStatusWhitelisted = "whitelisted" // whitelisted
filteringStatusRewritten = "rewritten" // all kinds of rewrites filteringStatusRewritten = "rewritten" // all kinds of rewrites
filteringStatusSafeSearch = "safe_search" // enforced safe search filteringStatusSafeSearch = "safe_search" // enforced safe search
filteringStatusProcessed = "processed" // not blocked, not white-listed entries
) )
// filteringStatusValues -- array with all possible filteringStatus values // filteringStatusValues -- array with all possible filteringStatus values
@ -32,6 +31,7 @@ var filteringStatusValues = []string{
filteringStatusAll, filteringStatusFiltered, filteringStatusBlocked, filteringStatusAll, filteringStatusFiltered, filteringStatusBlocked,
filteringStatusBlockedSafebrowsing, filteringStatusBlockedParental, filteringStatusBlockedSafebrowsing, filteringStatusBlockedParental,
filteringStatusWhitelisted, filteringStatusRewritten, filteringStatusSafeSearch, filteringStatusWhitelisted, filteringStatusRewritten, filteringStatusSafeSearch,
filteringStatusProcessed,
} }
// searchCriteria - every search request may contain a list of different search criteria // searchCriteria - every search request may contain a list of different search criteria
@ -48,12 +48,9 @@ func (c *searchCriteria) quickMatch(line string) bool {
// note that we do this only for a limited set of criteria // note that we do this only for a limited set of criteria
switch c.criteriaType { switch c.criteriaType {
case ctDomain: case ctDomainOrClient:
return c.quickMatchJSONValue(line, "QH") return c.quickMatchJSONValue(line, "QH") ||
case ctClient: c.quickMatchJSONValue(line, "IP")
return c.quickMatchJSONValue(line, "IP")
case ctQuestionType:
return c.quickMatchJSONValue(line, "QT")
default: default:
return true return true
} }
@ -80,29 +77,23 @@ func (c *searchCriteria) quickMatchJSONValue(line string, propertyName string) b
// nolint (gocyclo) // nolint (gocyclo)
func (c *searchCriteria) match(entry *logEntry) bool { func (c *searchCriteria) match(entry *logEntry) bool {
switch c.criteriaType { switch c.criteriaType {
case ctDomain: case ctDomainOrClient:
if c.strict && entry.QHost == c.value { if c.strict && entry.QHost == c.value {
return true return true
} }
if !c.strict && strings.Contains(entry.QHost, c.value) { if !c.strict && strings.Contains(entry.QHost, c.value) {
return true return true
} }
return false
case ctClient:
if c.strict && entry.IP == c.value { if c.strict && entry.IP == c.value {
return true return true
} }
if !c.strict && strings.Contains(entry.IP, c.value) { if !c.strict && strings.Contains(entry.IP, c.value) {
return true return true
} }
return false return false
case ctQuestionType:
if c.strict && entry.QType == c.value {
return true
}
if !c.strict && strings.Contains(entry.QType, c.value) {
return true
}
case ctFilteringStatus: case ctFilteringStatus:
res := entry.Result res := entry.Result
@ -110,7 +101,10 @@ func (c *searchCriteria) match(entry *logEntry) bool {
case filteringStatusAll: case filteringStatusAll:
return true return true
case filteringStatusFiltered: case filteringStatusFiltered:
return res.IsFiltered return res.IsFiltered ||
res.Reason == dnsfilter.NotFilteredWhiteList ||
res.Reason == dnsfilter.ReasonRewrite ||
res.Reason == dnsfilter.RewriteEtcHosts
case filteringStatusBlocked: case filteringStatusBlocked:
return res.IsFiltered && return res.IsFiltered &&
(res.Reason == dnsfilter.FilteredBlackList || (res.Reason == dnsfilter.FilteredBlackList ||
@ -120,20 +114,22 @@ func (c *searchCriteria) match(entry *logEntry) bool {
case filteringStatusBlockedSafebrowsing: case filteringStatusBlockedSafebrowsing:
return res.IsFiltered && res.Reason == dnsfilter.FilteredSafeBrowsing return res.IsFiltered && res.Reason == dnsfilter.FilteredSafeBrowsing
case filteringStatusWhitelisted: case filteringStatusWhitelisted:
return res.IsFiltered && res.Reason == dnsfilter.NotFilteredWhiteList return res.Reason == dnsfilter.NotFilteredWhiteList
case filteringStatusRewritten: case filteringStatusRewritten:
return res.IsFiltered && return (res.Reason == dnsfilter.ReasonRewrite ||
(res.Reason == dnsfilter.ReasonRewrite ||
res.Reason == dnsfilter.RewriteEtcHosts) res.Reason == dnsfilter.RewriteEtcHosts)
case filteringStatusSafeSearch: case filteringStatusSafeSearch:
return res.IsFiltered && res.Reason == dnsfilter.FilteredSafeSearch return res.IsFiltered && res.Reason == dnsfilter.FilteredSafeSearch
default:
return false case filteringStatusProcessed:
} return !(res.Reason == dnsfilter.FilteredBlackList ||
res.Reason == dnsfilter.FilteredBlockedService ||
res.Reason == dnsfilter.NotFilteredWhiteList)
default: default:
return false return false
} }
}
return false return false
} }