From 2e6e133af0f6ff9d56d71c82003ff02af149cf28 Mon Sep 17 00:00:00 2001 From: vnxme <46669194+vnxme@users.noreply.github.com> Date: Fri, 9 Aug 2024 20:06:01 +0300 Subject: [PATCH] DNS matcher (#231) * DNS matcher: initial commit * DNS matcher: add a note on TCP/UDP mixing --- imports.go | 1 + .../caddyfile_adapt/gd_matcher_dns.caddytest | 183 ++++++++ modules/l4dns/matcher.go | 412 ++++++++++++++++++ modules/l4dns/matcher_test.go | 169 +++++++ 4 files changed, 765 insertions(+) create mode 100644 integration/caddyfile_adapt/gd_matcher_dns.caddytest create mode 100644 modules/l4dns/matcher.go create mode 100644 modules/l4dns/matcher_test.go diff --git a/imports.go b/imports.go index 0ae54ce..81b2e1f 100644 --- a/imports.go +++ b/imports.go @@ -18,6 +18,7 @@ import ( // plugging in the standard modules for the layer4 app _ "github.com/mholt/caddy-l4/layer4" _ "github.com/mholt/caddy-l4/modules/l4clock" + _ "github.com/mholt/caddy-l4/modules/l4dns" _ "github.com/mholt/caddy-l4/modules/l4echo" _ "github.com/mholt/caddy-l4/modules/l4http" _ "github.com/mholt/caddy-l4/modules/l4postgres" diff --git a/integration/caddyfile_adapt/gd_matcher_dns.caddytest b/integration/caddyfile_adapt/gd_matcher_dns.caddytest new file mode 100644 index 0000000..009c1bc --- /dev/null +++ b/integration/caddyfile_adapt/gd_matcher_dns.caddytest @@ -0,0 +1,183 @@ +{ + layer4 { + tcp/:8053 { + @a dns { + allow_regexp ^(|[-0-9a-z]+\\.)example\\.com\\.$ + } + route @a { + proxy tcp/one.one.one.one:53 + } + @b tls + route @b { + tls + subroute { + @c dns { + allow example.com. NS + deny * * IN + default_deny + prefer_allow + } + route @c { + proxy tcp/one.one.one.one:53 + } + @d http + route @d { + proxy localhost:80 + } + } + } + } + udp/:53 { + @d dns { + deny_regexp * ^(MX|NS)$ + } + route @d { + proxy udp/one.one.one.one:53 + } + } + } +} +---------- +{ + "apps": { + "layer4": { + "servers": { + "srv0": { + "listen": [ + "tcp/:8053" + ], + "routes": [ + { + "match": [ + { + "dns": { + "allow": [ + { + "name_regexp": "^(|[-0-9a-z]+\\\\.)example\\\\.com\\\\.$" + } + ] + } + } + ], + "handle": [ + { + "handler": "proxy", + "upstreams": [ + { + "dial": [ + "tcp/one.one.one.one:53" + ] + } + ] + } + ] + }, + { + "match": [ + { + "tls": {} + } + ], + "handle": [ + { + "handler": "tls" + }, + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "proxy", + "upstreams": [ + { + "dial": [ + "tcp/one.one.one.one:53" + ] + } + ] + } + ], + "match": [ + { + "dns": { + "allow": [ + { + "name": "example.com.", + "type": "NS" + } + ], + "default_deny": true, + "deny": [ + { + "class": "IN" + } + ], + "prefer_allow": true + } + } + ] + }, + { + "handle": [ + { + "handler": "proxy", + "upstreams": [ + { + "dial": [ + "localhost:80" + ] + } + ] + } + ], + "match": [ + { + "http": [ + {} + ] + } + ] + } + ] + } + ] + } + ] + }, + "srv1": { + "listen": [ + "udp/:53" + ], + "routes": [ + { + "match": [ + { + "dns": { + "deny": [ + { + "type_regexp": "^(MX|NS)$" + } + ] + } + } + ], + "handle": [ + { + "handler": "proxy", + "upstreams": [ + { + "dial": [ + "udp/one.one.one.one:53" + ] + } + ] + } + ] + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/modules/l4dns/matcher.go b/modules/l4dns/matcher.go new file mode 100644 index 0000000..8a68d7d --- /dev/null +++ b/modules/l4dns/matcher.go @@ -0,0 +1,412 @@ +// Copyright 2024 VNXME +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package l4dns + +import ( + "context" + "encoding/binary" + "io" + "net" + "regexp" + "strings" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/miekg/dns" + + "github.com/mholt/caddy-l4/layer4" +) + +func init() { + caddy.RegisterModule(&MatchDNS{}) +} + +// MatchDNS is able to match connections that look like DNS protocol. +// Note: DNS messages sent via TCP are 2 bytes longer then those sent via UDP. Consequently, if Caddy listens on TCP, +// it has to proxy DNS messages to TCP upstreams only. The same is true for UDP. No TCP/UDP mixing is allowed. +// However, it's technically possible: an intermediary handler is required to add/strip 2 bytes before/after proxy. +// Please open a feature request and describe your use case if you need TCP/UDP mixing. +type MatchDNS struct { + // Allow contains an optional list of rules to match the question section of the DNS request message against. + // The matcher returns false if not matched by any of them (in the absence of any deny rules). + Allow MatchDNSRules `json:"allow,omitempty"` + // Deny contains an optional list of rules to match the question section of the DNS request message against. + // The matcher returns false if matched by any of them (in the absence of any allow rules). + Deny MatchDNSRules `json:"deny,omitempty"` + + // If DefaultDeny is true, DNS request messages that haven't been matched by any allow and deny rules are denied. + // The default action is allow. Use it to make the filter more restrictive when the rules aren't exhaustive. + DefaultDeny bool `json:"default_deny,omitempty"` + // If PreferAllow is true, DNS request messages that have been matched by both allow and deny rules are allowed. + // The default action is deny. Use it to make the filter less restrictive when the rules are mutually exclusive. + PreferAllow bool `json:"prefer_allow,omitempty"` +} + +// CaddyModule returns the Caddy module information. +func (m *MatchDNS) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "layer4.matchers.dns", + New: func() caddy.Module { return new(MatchDNS) }, + } +} + +// Match returns true if the connection bytes represent a valid DNS request message. +func (m *MatchDNS) Match(cx *layer4.Connection) (bool, error) { + var ( + msgBuf []byte + msgBytes uint16 + ) + + // Detect the connection protocol: TCP or UDP. + // Note: all non-TCP connections are treated as UDP, so no TCP packets could be matched while testing + // with net.Pipe() unless a valid cx.LocalAddr() response is provided using a fakeTCPConn wrapper. + if _, ok := cx.LocalAddr().(*net.TCPAddr); ok { + // Read the first 2 bytes, validate them and adjust the DNS message length + // Note: these 2 bytes represent the length of the remaining part of the packet + // as a big endian uint16 number. + err := binary.Read(cx, binary.BigEndian, &msgBytes) + if err != nil || msgBytes < dnsHeaderBytes || msgBytes > dns.MaxMsgSize { + return false, err + } + + // Read the remaining bytes + msgBuf = make([]byte, msgBytes) + _, err = io.ReadFull(cx, msgBuf) + if err != nil { + return false, nil + } + + // Validate the remaining connection buffer + // Note: if at least 1 byte remains, we can technically be sure, the protocol isn't DNS. + // This behaviour may be changed in the future if there are many false negative matches. + extraBuf := make([]byte, 1) + _, err = io.ReadFull(cx, extraBuf) + if err == nil { + return false, nil + } + } else { + // Read a minimum number of bytes + msgBuf = make([]byte, dnsHeaderBytes) + n, err := io.ReadAtLeast(cx, msgBuf, int(dnsHeaderBytes)) + if err != nil { + return false, nil + } + + // Read the remaining bytes and validate their length + var nn int + tmpBuf := make([]byte, dns.MinMsgSize) + for err == nil { + nn, err = io.ReadAtLeast(cx, tmpBuf, 1) + msgBuf = append(msgBuf, tmpBuf[:nn]...) + n += nn + } + if n > dns.MaxMsgSize { + return false, nil + } + msgBytes = uint16(n) + } + + // Unpack the DNS message with a third-party library + // Note: it doesn't return an error if there are any bytes remaining in the buffer after parsing has completed. + msg := new(dns.Msg) + if err := msg.Unpack(msgBuf); err != nil { + return false, nil + } + + // Ensure there are no extra bytes in the packet + if msg.Len() != int(msgBytes) { + return false, nil + } + + // Filter out invalid DNS request messages + if len(msg.Question) == 0 || msg.Response || msg.Rcode != dns.RcodeSuccess || msg.Zero { + return false, nil + } + + // Apply the allow and deny rules to the question section of the DNS request message + hasNoAllow, hasNoDeny := len(m.Allow) == 0, len(m.Deny) == 0 + if !(hasNoAllow && hasNoDeny) { + for _, q := range msg.Question { + // Filter out DNS request messages with invalid question classes + classValue, classFound := dns.ClassToString[q.Qclass] + if !classFound { + return false, nil + } + + // Filter out DNS request messages with invalid question types + typeValue, typeFound := dns.TypeToString[q.Qtype] + if !typeFound { + return false, nil + } + + denied := m.Deny.Match(cx.Context, classValue, typeValue, q.Name) + // If only deny rules are provided, filter out DNS request messages with denied question sections. + // In other words, allow all unless explicitly denied. + if hasNoAllow && !hasNoDeny && denied { + return false, nil + } + + allowed := m.Allow.Match(cx.Context, classValue, typeValue, q.Name) + // If only allow rules are provided, filter out DNS request messages with not allowed question sections. + // In other words, deny all unless explicitly allowed. + if hasNoDeny && !hasNoAllow && !allowed { + return false, nil + } + + // If both rules are provided and the question section is both allowed and denied, deny rules prevail + // unless the PreferAllow is set to true. If both rules are provided and the question section is + // neither allowed nor denied, it is allowed unless the DefaultDeny flag is set to true. + if denied { + if !allowed || !m.PreferAllow { + return false, nil + } + } else { + if !allowed && m.DefaultDeny { + return false, nil + } + } + } + } + + // Append the current DNS message to the messages list (it might be useful for other matchers or handlers) + appendMessage(cx, msg) + + return true, nil +} + +// Provision prepares m's allow and deny rules. +func (m *MatchDNS) Provision(cx caddy.Context) error { + err := m.Allow.Provision(cx) + if err != nil { + return err + } + err = m.Deny.Provision(cx) + if err != nil { + return err + } + return nil +} + +// UnmarshalCaddyfile sets up the MatchDNS from Caddyfile tokens. Syntax: +// +// dns { +// <*|name> [<*|type> [<*|class>]] +// <*|name_pattern> [<*|type_pattern> [<*|class_pattern>]] +// default_deny +// prefer_allow +// } +// dns +// +// Note: multiple allow and deny options are allowed. If default_deny is set, DNS request messages that haven't been +// matched by any allow and deny rules are denied (the default action is allow). If prefer_allow is set, DNS request +// messages that have been matched by both allow and deny rules are allowed (the default action is deny). An asterisk +// should be used to skip filtering the corresponding question section field, i.e. it will match any value provided. +func (m *MatchDNS) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + _, wrapper := d.Next(), d.Val() // consume wrapper name + + // No same-line arguments are supported + if d.CountRemainingArgs() != 0 { + return d.ArgErr() + } + + var hasDefaultDeny, hasPreferAllow bool + for nesting := d.Nesting(); d.NextBlock(nesting); { + optionName := d.Val() + switch optionName { + case "allow", "allow_regexp", "deny", "deny_regexp": + if d.CountRemainingArgs() == 0 || d.CountRemainingArgs() > 3 { + return d.ArgErr() + } + isRegexp := strings.HasSuffix(optionName, "regexp") + r := new(MatchDNSRule) + _, val := d.NextArg(), d.Val() + if val != dnsSpecialAny { + if isRegexp { + r.NameRegexp = val + } else { + r.Name = val + } + } + if d.NextArg() { + val = d.Val() + if val != dnsSpecialAny { + if isRegexp { + r.TypeRegexp = val + } else { + r.Type = val + } + } + } + if d.NextArg() { + val = d.Val() + if val != dnsSpecialAny { + if isRegexp { + r.ClassRegexp = val + } else { + r.Class = val + } + } + } + if strings.HasPrefix(optionName, "deny") { + m.Deny = append(m.Deny, r) + } else { + m.Allow = append(m.Allow, r) + } + case "default_deny": + if hasDefaultDeny { + return d.Errf("duplicate %s option '%s'", wrapper, optionName) + } + if d.CountRemainingArgs() > 0 { + return d.ArgErr() + } + m.DefaultDeny, hasDefaultDeny = true, true + case "prefer_allow": + if hasPreferAllow { + return d.Errf("duplicate %s option '%s'", wrapper, optionName) + } + if d.CountRemainingArgs() > 0 { + return d.ArgErr() + } + m.PreferAllow, hasPreferAllow = true, true + default: + return d.ArgErr() + } + + // No nested blocks are supported + if d.NextBlock(nesting + 1) { + return d.Errf("malformed %s option %s: nested blocks are not supported", wrapper, optionName) + } + } + + return nil +} + +// MatchDNSRules may contain a number of MatchDNSRule instances. An empty MatchDNSRules instance won't match anything. +type MatchDNSRules []*MatchDNSRule + +func (rs *MatchDNSRules) Match(cx context.Context, qClass string, qType string, qName string) bool { + for _, r := range *rs { + if r.Match(cx, qClass, qType, qName) { + return true + } + } + return false +} + +func (rs *MatchDNSRules) Provision(cx caddy.Context) error { + for _, r := range *rs { + if err := r.Provision(cx); err != nil { + return err + } + } + return nil +} + +// MatchDNSRule represents a set of filters to match against the question section of a DNS request message. +// Full and regular expression matching filters are supported. If both filters are provided for a single field, +// the full matcher is evaluated first. An empty MatchDNSRule will match anything. +type MatchDNSRule struct { + // Class may contain a value to match the question class. Use upper case letters, e.g. "IN", "CH", "ANY". + // See the full list of valid class values in dns.StringToClass. + Class string `json:"class,omitempty"` + // ClassRegexp may contain a regular expression to match the question class. E.g. "^(IN|CH)$". + // See the full list of valid class values in dns.StringToClass. + ClassRegexp string `json:"class_regexp,omitempty"` + // Name may contain a value to match the question domain name. E.g. "example.com.". + // The domain name is provided in lower case ending with a dot. + Name string `json:"name,omitempty"` + // NameRegexp may contain a regular expression to match the question domain name. + // E.g. "^(|[-0-9a-z]+\.)example\.com\.$". The domain name is provided in lower case ending with a dot. + NameRegexp string `json:"name_regexp,omitempty"` + // Type may contain a value to match the question type. Use upper case letters, e.g. "A", "MX", "NS". + // See the full list of valid type values in dns.StringToType. + Type string `json:"type,omitempty"` + // TypeRegexp may contain a regular expression to match the question type. E.g. "^(MX|NS)$". + // See the full list of valid type values in dns.StringToType. + TypeRegexp string `json:"type_regexp,omitempty"` + + classRegexp *regexp.Regexp + nameRegexp *regexp.Regexp + typeRegexp *regexp.Regexp +} + +func (r *MatchDNSRule) Match(cx context.Context, qClass string, qType string, qName string) bool { + repl := cx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + + // Validate the question class + classFilter := repl.ReplaceAll(r.Class, "") + if (len(classFilter) > 0 && qClass != classFilter) || + len(r.ClassRegexp) > 0 && !r.classRegexp.MatchString(qClass) { + return false + } + + // Validate the question type + typeFilter := repl.ReplaceAll(r.Type, "") + if (len(typeFilter) > 0 && qType != typeFilter) || + len(r.TypeRegexp) > 0 && !r.typeRegexp.MatchString(qType) { + return false + } + + // Validate the question domain name + nameFilter := repl.ReplaceAll(r.Name, "") + if (len(nameFilter) > 0 && qName != nameFilter) || + (len(r.NameRegexp) > 0 && !r.nameRegexp.MatchString(qName)) { + return false + } + + return true +} + +func (r *MatchDNSRule) Provision(_ caddy.Context) (err error) { + repl := caddy.NewReplacer() + r.classRegexp, err = regexp.Compile(repl.ReplaceAll(r.ClassRegexp, "")) + if err != nil { + return err + } + r.typeRegexp, err = regexp.Compile(repl.ReplaceAll(r.TypeRegexp, "")) + if err != nil { + return err + } + r.nameRegexp, err = regexp.Compile(repl.ReplaceAll(r.NameRegexp, "")) + if err != nil { + return err + } + return nil +} + +// Interface guards +var ( + _ caddy.Provisioner = (*MatchDNS)(nil) + _ caddyfile.Unmarshaler = (*MatchDNS)(nil) + _ layer4.ConnMatcher = (*MatchDNS)(nil) + + _ caddy.Provisioner = (*MatchDNSRules)(nil) + _ caddy.Provisioner = (*MatchDNSRule)(nil) +) + +const ( + dnsHeaderBytes uint16 = 12 // read this many bytes to parse a DNS message header (equals dns.headerSize) + dnsMessagesKey = "dns_messages" + dnsSpecialAny = "*" +) + +func appendMessage(cx *layer4.Connection, msg *dns.Msg) { + var messages []*dns.Msg + if val := cx.GetVar(dnsMessagesKey); val != nil { + messages = val.([]*dns.Msg) + } + messages = append(messages, msg) + cx.SetVar(dnsMessagesKey, messages) +} diff --git a/modules/l4dns/matcher_test.go b/modules/l4dns/matcher_test.go new file mode 100644 index 0000000..45645fe --- /dev/null +++ b/modules/l4dns/matcher_test.go @@ -0,0 +1,169 @@ +// Copyright 2024 VNXME +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package l4dns + +import ( + "context" + "io" + "net" + "testing" + + "github.com/caddyserver/caddy/v2" + "go.uber.org/zap" + + "github.com/mholt/caddy-l4/layer4" +) + +func assertNoError(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("Unexpected error: %s\n", err) + } +} + +func Test_MatchDNS_Match(t *testing.T) { + type test struct { + matcher *MatchDNS + data []byte + shouldMatch bool + shouldFakeTCP bool + } + + tests := []test{ + {matcher: &MatchDNS{}, data: []byte{}, shouldMatch: false}, + + {matcher: &MatchDNS{}, data: udpPacketAppleComA[:12], shouldMatch: false}, + {matcher: &MatchDNS{}, data: udpPacketGoogleComA[:14], shouldMatch: false}, + {matcher: &MatchDNS{}, data: tcpPacketAppleComA[:14], shouldMatch: false, shouldFakeTCP: true}, + {matcher: &MatchDNS{}, data: tcpPacketGoogleComA[:16], shouldMatch: false, shouldFakeTCP: true}, + + {matcher: &MatchDNS{}, data: udpPacketAppleComA, shouldMatch: true}, + {matcher: &MatchDNS{}, data: udpPacketGoogleComA, shouldMatch: true}, + {matcher: &MatchDNS{}, data: tcpPacketAppleComA, shouldMatch: true, shouldFakeTCP: true}, + {matcher: &MatchDNS{}, data: tcpPacketGoogleComA, shouldMatch: true, shouldFakeTCP: true}, + + {matcher: &MatchDNS{Allow: MatchDNSRules{&MatchDNSRule{Name: "example.com.", Type: "NS"}}}, + data: tcpPacketExampleComA, shouldMatch: false, shouldFakeTCP: true}, + {matcher: &MatchDNS{Allow: MatchDNSRules{&MatchDNSRule{Name: "example.com.", Type: "A", Class: "IN"}}}, + data: tcpPacketExampleComA, shouldMatch: true, shouldFakeTCP: true}, + {matcher: &MatchDNS{Allow: MatchDNSRules{&MatchDNSRule{TypeRegexp: "^(MX|NS)$"}}}, + data: tcpPacketExampleComA, shouldMatch: false, shouldFakeTCP: true}, + {matcher: &MatchDNS{Allow: MatchDNSRules{&MatchDNSRule{NameRegexp: "^(|[-0-9a-z]+\\.)example\\.com\\.$"}}}, + data: tcpPacketExampleComA, shouldMatch: true, shouldFakeTCP: true}, + + {matcher: &MatchDNS{Deny: MatchDNSRules{&MatchDNSRule{Name: ".", Class: "IN"}}}, + data: tcpPacketDotNS, shouldMatch: false, shouldFakeTCP: true}, + {matcher: &MatchDNS{Deny: MatchDNSRules{&MatchDNSRule{Type: "A"}}}, + data: tcpPacketDotNS, shouldMatch: true, shouldFakeTCP: true}, + + {matcher: &MatchDNS{ + Allow: MatchDNSRules{&MatchDNSRule{Name: "example.com.", Type: "A"}}, + Deny: MatchDNSRules{&MatchDNSRule{Class: "IN"}}, + }, data: tcpPacketExampleComA, shouldMatch: false, shouldFakeTCP: true}, + {matcher: &MatchDNS{ + Allow: MatchDNSRules{&MatchDNSRule{Name: "example.com.", Type: "NS"}}, + Deny: MatchDNSRules{&MatchDNSRule{Type: "MX"}}, + }, data: tcpPacketExampleComA, shouldMatch: true, shouldFakeTCP: true}, + {matcher: &MatchDNS{ + Allow: MatchDNSRules{&MatchDNSRule{Name: "example.com.", Type: "NS"}}, + Deny: MatchDNSRules{&MatchDNSRule{Type: "MX"}}, + DefaultDeny: true, + }, data: tcpPacketExampleComA, shouldMatch: false, shouldFakeTCP: true}, + {matcher: &MatchDNS{ + Allow: MatchDNSRules{&MatchDNSRule{Name: "example.com.", Type: "A"}}, + Deny: MatchDNSRules{&MatchDNSRule{Class: "IN"}}, + PreferAllow: true, + }, data: tcpPacketExampleComA, shouldMatch: true, shouldFakeTCP: true}, + } + + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + + for i, tc := range tests { + func() { + err := tc.matcher.Provision(ctx) + assertNoError(t, err) + + in, out := net.Pipe() + defer func() { + _, _ = io.Copy(io.Discard, out) + _ = out.Close() + }() + + if tc.shouldFakeTCP { + out = &fakeTCPConn{Conn: out} + } + + cx := layer4.WrapConnection(out, []byte{}, zap.NewNop()) + go func() { + _, err := in.Write(tc.data) + assertNoError(t, err) + _ = in.Close() + }() + + matched, err := tc.matcher.Match(cx) + assertNoError(t, err) + + if matched != tc.shouldMatch { + if tc.shouldMatch { + t.Fatalf("test %d: matcher did not match | %+v\n", i, tc.matcher) + } else { + t.Fatalf("test %d: matcher should not match | %+v\n", i, tc.matcher) + } + } + }() + } +} + +type fakeTCPConn struct { + net.Conn +} + +func (c *fakeTCPConn) LocalAddr() net.Addr { + return &net.TCPAddr{} +} + +// Interface guard +var _ net.Conn = (*fakeTCPConn)(nil) + +// Packet examples +var tcpPacketAppleComA = []byte{ + 0, 27, + 126, 193, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 5, 97, 112, 112, 108, 101, 3, 99, 111, 109, 0, 0, 1, 0, 1, // apple.com (A, IN) +} +var udpPacketAppleComA = []byte{ + 0, 7, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 5, 97, 112, 112, 108, 101, 3, 99, 111, 109, 0, 0, 1, 0, 1, // apple.com (A, IN) +} +var tcpPacketGoogleComA = []byte{ + 0, 28, + 207, 90, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 6, 103, 111, 111, 103, 108, 101, 3, 99, 111, 109, 0, 0, 5, 0, 1, // google.com. (A, IN) +} +var udpPacketGoogleComA = []byte{ + 0, 11, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 6, 103, 111, 111, 103, 108, 101, 3, 99, 111, 109, 0, 0, 1, 0, 1, // google.com. (A, IN) +} +var tcpPacketExampleComA = []byte{ + 0, 29, + 101, 3, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 7, 101, 120, 97, 109, 112, 108, 101, 3, 99, 111, 109, 0, 0, 1, 0, 1, // example.com. (A, IN) +} +var tcpPacketDotNS = []byte{ + 0, 17, + 213, 147, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 2, 0, 1, // . (NS, IN) +}