middleware/log: allows logging based on response classes (#325)
Add the ability to add a class of responses to be logged; success, denial or error. The default is to log everything (all). Fixes #258
This commit is contained in:
parent
caa3976bfe
commit
c22b7b2252
13 changed files with 348 additions and 108 deletions
2
middleware/cache/README.md
vendored
2
middleware/cache/README.md
vendored
|
@ -20,7 +20,7 @@ Or if you want more control:
|
|||
|
||||
~~~ txt
|
||||
cache [ttl] [zones...] {
|
||||
noerror capacity [ttl]
|
||||
success capacity [ttl]
|
||||
denial capacity [ttl]
|
||||
}
|
||||
~~~
|
||||
|
|
4
middleware/cache/cache.go
vendored
4
middleware/cache/cache.go
vendored
|
@ -60,7 +60,7 @@ type ResponseWriter struct {
|
|||
// WriteMsg implements the dns.ResponseWriter interface.
|
||||
func (c *ResponseWriter) WriteMsg(res *dns.Msg) error {
|
||||
do := false
|
||||
mt, opt := response.Classify(res)
|
||||
mt, opt := response.Typify(res)
|
||||
if opt != nil {
|
||||
do = opt.Do()
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ func (c *ResponseWriter) set(m *dns.Msg, key string, mt response.Type, duration
|
|||
}
|
||||
|
||||
switch mt {
|
||||
case response.Success, response.Delegation:
|
||||
case response.NoError, response.Delegation:
|
||||
i := newItem(m, duration)
|
||||
c.pcache.Add(key, i)
|
||||
|
||||
|
|
2
middleware/cache/cache_test.go
vendored
2
middleware/cache/cache_test.go
vendored
|
@ -94,7 +94,7 @@ func TestCache(t *testing.T) {
|
|||
m = cacheMsg(m, tc)
|
||||
do := tc.in.Do
|
||||
|
||||
mt, _ := response.Classify(m)
|
||||
mt, _ := response.Typify(m)
|
||||
k := key(m, mt, do)
|
||||
crr.set(m, k, mt, c.pttl)
|
||||
|
||||
|
|
4
middleware/cache/item.go
vendored
4
middleware/cache/item.go
vendored
|
@ -87,7 +87,7 @@ func setMsgTTL(m *dns.Msg, ttl uint32) {
|
|||
}
|
||||
|
||||
func minMsgTTL(m *dns.Msg, mt response.Type) time.Duration {
|
||||
if mt != response.Success && mt != response.NameError && mt != response.NoData {
|
||||
if mt != response.NoError && mt != response.NameError && mt != response.NoData {
|
||||
return 0
|
||||
}
|
||||
|
||||
|
@ -98,7 +98,7 @@ func minMsgTTL(m *dns.Msg, mt response.Type) time.Duration {
|
|||
if r.Header().Rrtype == dns.TypeSOA {
|
||||
return time.Duration(r.(*dns.SOA).Minttl) * time.Second
|
||||
}
|
||||
case response.Success, response.Delegation:
|
||||
case response.NoError, response.Delegation:
|
||||
if r.Header().Ttl < uint32(minTTL.Seconds()) {
|
||||
minTTL = time.Duration(r.Header().Ttl) * time.Second
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ func New(zones []string, keys []*DNSKEY, next middleware.Handler) Dnssec {
|
|||
func (d Dnssec) Sign(state request.Request, zone string, now time.Time) *dns.Msg {
|
||||
req := state.Req
|
||||
|
||||
mt, _ := response.Classify(req) // TODO(miek): need opt record here?
|
||||
mt, _ := response.Typify(req) // TODO(miek): need opt record here?
|
||||
if mt == response.Delegation {
|
||||
return req
|
||||
}
|
||||
|
|
|
@ -1,34 +1,53 @@
|
|||
# log
|
||||
|
||||
`log` enables request logging. The request log is also known in some vernacular as an access log.
|
||||
`log` enables query logging.
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~
|
||||
~~~ txt
|
||||
log
|
||||
~~~
|
||||
|
||||
* With no arguments, a query log entry is written to query.log in the common log format for all requests
|
||||
(base name = .).
|
||||
|
||||
~~~
|
||||
~~~ txt
|
||||
log file
|
||||
~~~
|
||||
|
||||
* file is the log file to create (or append to). The base path is assumed to be . .
|
||||
* file is the log file to create (or append to). The base name is assumed to be '.' .
|
||||
|
||||
~~~
|
||||
log name file [format]
|
||||
~~~ txt
|
||||
log [name] [file] [format]
|
||||
~~~
|
||||
|
||||
* `name` is the base name to match in order to be logged
|
||||
* `file` is the log file to create (or append to)
|
||||
* `format` is the log format to use (default is Common Log Format)
|
||||
|
||||
You can further specify the class of responses that get logged:
|
||||
|
||||
~~~ txt
|
||||
log [name] [file] [format] {
|
||||
class [success|denial|error|all]
|
||||
}
|
||||
~~~
|
||||
|
||||
Here *success*, *denial* and *error* denotes the class of responses that should be logged. The
|
||||
classes have the following meaning:
|
||||
|
||||
* `success`: successful response
|
||||
* `denial`: either NXDOMAIN or NODATA (name exists, type does not)
|
||||
* `error`: SERVFAIL, NOTIMP, REFUSED, etc. Any that indicated the remove server is not willing to
|
||||
resolve the request.
|
||||
* `all`: the default is nothing is specified.
|
||||
|
||||
If no class is specified it defaults to *all*.
|
||||
|
||||
## Log File
|
||||
|
||||
The log file can be any filename. It could also be stdout or stderr to write the log to the console,
|
||||
or syslog to write to the system log (except on Windows). If the log file does not exist beforehand,
|
||||
The log file can be any filename. It could also be *stdout* or *stderr* to write the log to the console,
|
||||
or *syslog* to write to the system log (except on Windows). If the log file does not exist beforehand,
|
||||
CoreDNS will create it before appending to it.
|
||||
|
||||
## Log Format
|
||||
|
@ -53,6 +72,11 @@ The following place holders are supported:
|
|||
* `{>id}`: query ID
|
||||
* `{>opcode}`: query OPCODE
|
||||
|
||||
The default Common Log Format is:
|
||||
|
||||
~~~ txt
|
||||
`{remote} - [{when}] "{type} {class} {name} {proto} {>do} {>bufsize}" {rcode} {size} {duration}`
|
||||
~~~
|
||||
|
||||
## Examples
|
||||
|
||||
|
@ -67,3 +91,11 @@ Custom log format:
|
|||
~~~
|
||||
log . ../query.log "{proto} Request: {name} {type} {>id}"
|
||||
~~~
|
||||
|
||||
Only log denials for example.org (and below to a file)
|
||||
|
||||
~~~
|
||||
log example.org example-query-log {
|
||||
class denial
|
||||
}
|
||||
~~~
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/miekg/coredns/middleware/pkg/dnsrecorder"
|
||||
"github.com/miekg/coredns/middleware/pkg/rcode"
|
||||
"github.com/miekg/coredns/middleware/pkg/replacer"
|
||||
"github.com/miekg/coredns/middleware/pkg/response"
|
||||
"github.com/miekg/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
@ -27,7 +28,10 @@ type Logger struct {
|
|||
func (l Logger) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
state := request.Request{W: w, Req: r}
|
||||
for _, rule := range l.Rules {
|
||||
if middleware.Name(rule.NameScope).Matches(state.Name()) {
|
||||
if !middleware.Name(rule.NameScope).Matches(state.Name()) {
|
||||
continue
|
||||
}
|
||||
|
||||
responseRecorder := dnsrecorder.New(w)
|
||||
rc, err := l.Next.ServeDNS(ctx, responseRecorder, r)
|
||||
|
||||
|
@ -42,22 +46,28 @@ func (l Logger) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
|
|||
state.SizeAndDo(answer)
|
||||
|
||||
metrics.Report(state, metrics.Dropped, rcode.ToString(rc), answer.Len(), time.Now())
|
||||
|
||||
w.WriteMsg(answer)
|
||||
}
|
||||
rc = 0
|
||||
}
|
||||
|
||||
class, _ := response.Classify(responseRecorder.Msg)
|
||||
if rule.Class == response.All || rule.Class == class {
|
||||
rep := replacer.New(r, responseRecorder, CommonLogEmptyValue)
|
||||
rule.Log.Println(rep.Replace(rule.Format))
|
||||
}
|
||||
|
||||
return rc, err
|
||||
|
||||
}
|
||||
}
|
||||
return l.Next.ServeDNS(ctx, w, r)
|
||||
}
|
||||
|
||||
// Rule configures the logging middleware.
|
||||
type Rule struct {
|
||||
NameScope string
|
||||
Class response.Class
|
||||
OutputFile string
|
||||
Format string
|
||||
Log *log.Logger
|
||||
|
|
|
@ -7,21 +7,15 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/miekg/coredns/middleware/pkg/dnsrecorder"
|
||||
"github.com/miekg/coredns/middleware/pkg/response"
|
||||
"github.com/miekg/coredns/middleware/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type erroringMiddleware struct{}
|
||||
|
||||
func (erroringMiddleware) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
return dns.RcodeServerFailure, nil
|
||||
}
|
||||
|
||||
func TestLoggedStatus(t *testing.T) {
|
||||
var f bytes.Buffer
|
||||
var next erroringMiddleware
|
||||
rule := Rule{
|
||||
NameScope: ".",
|
||||
Format: DefaultLogFormat,
|
||||
|
@ -30,7 +24,7 @@ func TestLoggedStatus(t *testing.T) {
|
|||
|
||||
logger := Logger{
|
||||
Rules: []Rule{rule},
|
||||
Next: next,
|
||||
Next: test.ErrorHandler(),
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
|
@ -41,11 +35,67 @@ func TestLoggedStatus(t *testing.T) {
|
|||
|
||||
rcode, _ := logger.ServeDNS(ctx, rec, r)
|
||||
if rcode != 0 {
|
||||
t.Error("Expected rcode to be 0 - was", rcode)
|
||||
t.Errorf("Expected rcode to be 0 - was: %d", rcode)
|
||||
}
|
||||
|
||||
logged := f.String()
|
||||
if !strings.Contains(logged, "A IN example.org. udp false 512") {
|
||||
t.Error("Expected it to be logged. Logged string -", logged)
|
||||
t.Errorf("Expected it to be logged. Logged string: %s", logged)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggedClassDenial(t *testing.T) {
|
||||
var f bytes.Buffer
|
||||
rule := Rule{
|
||||
NameScope: ".",
|
||||
Format: DefaultLogFormat,
|
||||
Log: log.New(&f, "", 0),
|
||||
Class: response.Denial,
|
||||
}
|
||||
|
||||
logger := Logger{
|
||||
Rules: []Rule{rule},
|
||||
Next: test.ErrorHandler(),
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
r := new(dns.Msg)
|
||||
r.SetQuestion("example.org.", dns.TypeA)
|
||||
|
||||
rec := dnsrecorder.New(&test.ResponseWriter{})
|
||||
|
||||
logger.ServeDNS(ctx, rec, r)
|
||||
|
||||
logged := f.String()
|
||||
if len(logged) != 0 {
|
||||
t.Errorf("Expected it not to be logged, but got string: %s", logged)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggedClassError(t *testing.T) {
|
||||
var f bytes.Buffer
|
||||
rule := Rule{
|
||||
NameScope: ".",
|
||||
Format: DefaultLogFormat,
|
||||
Log: log.New(&f, "", 0),
|
||||
Class: response.Error,
|
||||
}
|
||||
|
||||
logger := Logger{
|
||||
Rules: []Rule{rule},
|
||||
Next: test.ErrorHandler(),
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
r := new(dns.Msg)
|
||||
r.SetQuestion("example.org.", dns.TypeA)
|
||||
|
||||
rec := dnsrecorder.New(&test.ResponseWriter{})
|
||||
|
||||
logger.ServeDNS(ctx, rec, r)
|
||||
|
||||
logged := f.String()
|
||||
if !strings.Contains(logged, "SERVFAIL") {
|
||||
t.Errorf("Expected it to be logged. Logged string: %s", logged)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/miekg/coredns/core/dnsserver"
|
||||
"github.com/miekg/coredns/middleware"
|
||||
"github.com/miekg/coredns/middleware/pkg/response"
|
||||
|
||||
"github.com/hashicorp/go-syslog"
|
||||
"github.com/mholt/caddy"
|
||||
|
@ -105,6 +106,26 @@ func logParse(c *caddy.Controller) ([]Rule, error) {
|
|||
Format: format,
|
||||
})
|
||||
}
|
||||
|
||||
// Class refinements in an extra block.
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
// class followed by all, denial, error or success.
|
||||
case "class":
|
||||
classes := c.RemainingArgs()
|
||||
if len(classes) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
cls, err := response.ClassFromString(classes[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// update class and the last added Rule (bit icky)
|
||||
rules[len(rules)-1].Class = cls
|
||||
default:
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
|
|
|
@ -3,6 +3,8 @@ package log
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/coredns/middleware/pkg/response"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
|
@ -62,6 +64,31 @@ func TestLogParse(t *testing.T) {
|
|||
OutputFile: "log.txt",
|
||||
Format: "{when}",
|
||||
}}},
|
||||
|
||||
{`log example.org log.txt {
|
||||
class all
|
||||
}`, false, []Rule{{
|
||||
NameScope: "example.org.",
|
||||
OutputFile: "log.txt",
|
||||
Format: CommonLogFormat,
|
||||
Class: response.All,
|
||||
}}},
|
||||
{`log example.org log.txt {
|
||||
class denial
|
||||
}`, false, []Rule{{
|
||||
NameScope: "example.org.",
|
||||
OutputFile: "log.txt",
|
||||
Format: CommonLogFormat,
|
||||
Class: response.Denial,
|
||||
}}},
|
||||
{`log {
|
||||
class denial
|
||||
}`, false, []Rule{{
|
||||
NameScope: ".",
|
||||
OutputFile: DefaultLogFilename,
|
||||
Format: CommonLogFormat,
|
||||
Class: response.Denial,
|
||||
}}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := caddy.NewTestController("dns", test.inputLogRules)
|
||||
|
@ -92,6 +119,11 @@ func TestLogParse(t *testing.T) {
|
|||
t.Errorf("Test %d expected %dth LogRule Format to be %s , but got %s",
|
||||
i, j, test.expectedLogRules[j].Format, actualLogRule.Format)
|
||||
}
|
||||
|
||||
if actualLogRule.Class != test.expectedLogRules[j].Class {
|
||||
t.Errorf("Test %d expected %dth LogRule Class to be %s , but got %s",
|
||||
i, j, test.expectedLogRules[j].Class, actualLogRule.Class)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,74 +1,73 @@
|
|||
package response
|
||||
|
||||
import "github.com/miekg/dns"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
// Type is the type of the message
|
||||
type Type int
|
||||
|
||||
const (
|
||||
// Success indicates a positive reply
|
||||
Success Type = iota
|
||||
// NameError is a NXDOMAIN in header, SOA in auth.
|
||||
NameError
|
||||
// NoData indicated name found, but not the type: NOERROR in header, SOA in auth.
|
||||
NoData
|
||||
// Delegation is a msg with a pointer to another nameserver: NOERROR in header, NS in auth, optionally fluff in additional (not checked).
|
||||
Delegation
|
||||
// OtherError indicated any other error: don't cache these.
|
||||
OtherError
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func (t Type) String() string {
|
||||
switch t {
|
||||
// Class holds sets of Types
|
||||
type Class int
|
||||
|
||||
const (
|
||||
// All is a meta class encompassing all the classes.
|
||||
All Class = iota
|
||||
// Success is a class for a successful response.
|
||||
Success
|
||||
// Denial is a class for denying existence (NXDOMAIN, or a nodata: type does not exist)
|
||||
Denial
|
||||
// Error is a class for errors, right now defined as not Success and not Denial
|
||||
Error
|
||||
)
|
||||
|
||||
func (c Class) String() string {
|
||||
switch c {
|
||||
case All:
|
||||
return "all"
|
||||
case Success:
|
||||
return "NOERROR"
|
||||
case NameError:
|
||||
return "NXDOMAIN"
|
||||
case NoData:
|
||||
return "NODATA"
|
||||
case Delegation:
|
||||
return "DELEGATION"
|
||||
case OtherError:
|
||||
return "OTHERERROR"
|
||||
return "success"
|
||||
case Denial:
|
||||
return "denial"
|
||||
case Error:
|
||||
return "error"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Classify classifies a message, it returns the Type.
|
||||
func Classify(m *dns.Msg) (Type, *dns.OPT) {
|
||||
opt := m.IsEdns0()
|
||||
|
||||
if len(m.Answer) > 0 && m.Rcode == dns.RcodeSuccess {
|
||||
return Success, opt
|
||||
// ClassFromString returns the class from the string s. If not class matches
|
||||
// the All class and an error are returned
|
||||
func ClassFromString(s string) (Class, error) {
|
||||
switch s {
|
||||
case "all":
|
||||
return All, nil
|
||||
case "success":
|
||||
return Success, nil
|
||||
case "denial":
|
||||
return Denial, nil
|
||||
case "error":
|
||||
return Error, nil
|
||||
}
|
||||
return All, fmt.Errorf("invalid Class: %s", s)
|
||||
}
|
||||
|
||||
soa := false
|
||||
ns := 0
|
||||
for _, r := range m.Ns {
|
||||
if r.Header().Rrtype == dns.TypeSOA {
|
||||
soa = true
|
||||
continue
|
||||
}
|
||||
if r.Header().Rrtype == dns.TypeNS {
|
||||
ns++
|
||||
}
|
||||
// Classify classifies a dns message: it returns its Class.
|
||||
func Classify(m *dns.Msg) (Class, *dns.OPT) {
|
||||
t, o := Typify(m)
|
||||
return classify(t), o
|
||||
}
|
||||
|
||||
// Check length of different sections, and drop stuff that is just to large? TODO(miek).
|
||||
if soa && m.Rcode == dns.RcodeSuccess {
|
||||
return NoData, opt
|
||||
// Does need to be exported?
|
||||
func classify(t Type) Class {
|
||||
switch t {
|
||||
case NoError, Delegation:
|
||||
return Success
|
||||
case NameError, NoData:
|
||||
return Denial
|
||||
case OtherError:
|
||||
fallthrough
|
||||
default:
|
||||
return Error
|
||||
}
|
||||
if soa && m.Rcode == dns.RcodeNameError {
|
||||
return NameError, opt
|
||||
}
|
||||
|
||||
if ns > 0 && ns == len(m.Ns) && m.Rcode == dns.RcodeSuccess {
|
||||
return Delegation, opt
|
||||
}
|
||||
|
||||
if m.Rcode == dns.RcodeSuccess {
|
||||
return Success, opt
|
||||
}
|
||||
|
||||
return OtherError, opt
|
||||
// never reached
|
||||
return All
|
||||
}
|
||||
|
|
96
middleware/pkg/response/typify.go
Normal file
96
middleware/pkg/response/typify.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package response
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Type is the type of the message
|
||||
type Type int
|
||||
|
||||
const (
|
||||
// Success indicates a positive reply
|
||||
NoError Type = iota
|
||||
// NameError is a NXDOMAIN in header, SOA in auth.
|
||||
NameError
|
||||
// NoData indicated name found, but not the type: NOERROR in header, SOA in auth.
|
||||
NoData
|
||||
// Delegation is a msg with a pointer to another nameserver: NOERROR in header, NS in auth, optionally fluff in additional (not checked).
|
||||
Delegation
|
||||
// OtherError indicated any other error: don't cache these.
|
||||
OtherError
|
||||
)
|
||||
|
||||
func (t Type) String() string {
|
||||
switch t {
|
||||
case NoError:
|
||||
return "NOERROR"
|
||||
case NameError:
|
||||
return "NXDOMAIN"
|
||||
case NoData:
|
||||
return "NODATA"
|
||||
case Delegation:
|
||||
return "DELEGATION"
|
||||
case OtherError:
|
||||
return "OTHERERROR"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// TypeFromString returns the type from the string s. If not type matches
|
||||
// the OtherError type and an error are returned.
|
||||
func TypeFromString(s string) (Type, error) {
|
||||
switch s {
|
||||
case "NOERROR":
|
||||
return NoError, nil
|
||||
case "NXDOMAIN":
|
||||
return NameError, nil
|
||||
case "NODATA":
|
||||
return NoData, nil
|
||||
case "DELEGATION":
|
||||
return Delegation, nil
|
||||
case "OTHERERROR":
|
||||
return OtherError, nil
|
||||
}
|
||||
return NoError, fmt.Errorf("invalid Type: %s", s)
|
||||
}
|
||||
|
||||
// Typify classifies a message, it returns the Type.
|
||||
func Typify(m *dns.Msg) (Type, *dns.OPT) {
|
||||
opt := m.IsEdns0()
|
||||
|
||||
if len(m.Answer) > 0 && m.Rcode == dns.RcodeSuccess {
|
||||
return NoError, opt
|
||||
}
|
||||
|
||||
soa := false
|
||||
ns := 0
|
||||
for _, r := range m.Ns {
|
||||
if r.Header().Rrtype == dns.TypeSOA {
|
||||
soa = true
|
||||
continue
|
||||
}
|
||||
if r.Header().Rrtype == dns.TypeNS {
|
||||
ns++
|
||||
}
|
||||
}
|
||||
|
||||
// Check length of different sections, and drop stuff that is just to large? TODO(miek).
|
||||
if soa && m.Rcode == dns.RcodeSuccess {
|
||||
return NoData, opt
|
||||
}
|
||||
if soa && m.Rcode == dns.RcodeNameError {
|
||||
return NameError, opt
|
||||
}
|
||||
|
||||
if ns > 0 && ns == len(m.Ns) && m.Rcode == dns.RcodeSuccess {
|
||||
return Delegation, opt
|
||||
}
|
||||
|
||||
if m.Rcode == dns.RcodeSuccess {
|
||||
return NoError, opt
|
||||
}
|
||||
|
||||
return OtherError, opt
|
||||
}
|
|
@ -10,7 +10,7 @@ import (
|
|||
|
||||
func TestClassifyDelegation(t *testing.T) {
|
||||
m := delegationMsg()
|
||||
mt, _ := Classify(m)
|
||||
mt, _ := Typify(m)
|
||||
if mt != Delegation {
|
||||
t.Errorf("message is wrongly classified, expected delegation, got %d", mt)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue