diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 73616035556..9f03381a1fd 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -47,6 +47,7 @@ https://github.com/elastic/beats/compare/1.0.0...master[Check the HEAD diff] - Make logstash output compression level configurable. {pull}630[630] *Packetbeat* +- Add support for capturing DNS over TCP network traffic. {pull}486[486] {pull}554[554] *Topbeat* - Group all cpu usage per core statistics and export them optionally if cpu_per_core is configured {pull}496[496] diff --git a/packetbeat/protos/dns/README.md b/packetbeat/protos/dns/README.md new file mode 100644 index 00000000000..9cc27c567b8 --- /dev/null +++ b/packetbeat/protos/dns/README.md @@ -0,0 +1,43 @@ +#### UDP + +**Parsing** + +1. Attempt to decode each UDP packet. +2. If it succeeds, a transaction is sent. + +**Error management** +* Debug information is printed if: + * A packet fails to decode. + +* Error Notes are published if: + * Never + +#### TCP + +**Parsing** + +1. Fetch the first two bytes of a message containing the length of the message ([RFC 1035](https://www.ietf.org/rfc/rfc1035.txt)). +2. Fill the buffer ```DnsStream.rawData``` with each new ```Parse```. +3. Once the buffer has the expected length (first two bytes), it is decoded and the message is published. + +**Error management** +* Debug information is printed if: + * A message has an unexpected length at any point of the transmission (```Parse```, ```GapInStream```, ```ReceivedFin```). + * A message fails to decode. + +* Error Notes are published if: + * A response following a request (```dnsConnectionData.prevRequest```) fails to decode. + * A response following a request (```dnsConnectionData.prevRequest```) has an unexpected length at any point of the transmission (```Parse```, ```GapInStream```, ```ReceivedFin```). + +When response error Notes are linked to the previous request, the transaction is then published and removed from the cache (see ```publishResponseError()```). + +#### TODO + +**General** +* Publish an event with Notes when a Query or a lone Response cannot be decoded. +* Add EDNS and DNSSEC support (consider using miekg/dns instead + of gopacket). +* Consider adding ICMP support to + - correlate ICMP type 3, code 4 (datagram too big) with DNS messages, + - correlate ICMP type 3, code 13 (administratively prohibited) or + ICMP type 3, code 3 (port unreachable) with blocked DNS messages. diff --git a/packetbeat/protos/dns/dns.go b/packetbeat/protos/dns/dns.go index 5db7c2c0201..4c25bb901dc 100644 --- a/packetbeat/protos/dns/dns.go +++ b/packetbeat/protos/dns/dns.go @@ -4,15 +4,6 @@ // RFC 4035 (DNS Security Extensions), but since those specifications only // add backwards compatible features there will be no issues handling the // messages. -// -// Future Additions: -// * Publish a message when Query packets are received that cannot be decoded. -// * Add EDNS and DNSSEC support (consider using miekg/dns instead -// of gopacket). -// * Consider adding ICMP support to -// - correlate ICMP type 3, code 4 (datagram too big) with DNS messages, -// - correlate ICMP type 3, code 13 (administratively prohibited) or -// ICMP type 3, code 3 (port unreachable) with blocked DNS messages. package dns @@ -27,9 +18,7 @@ import ( "github.com/elastic/beats/libbeat/publisher" "github.com/elastic/beats/packetbeat/config" - "github.com/elastic/beats/packetbeat/procs" "github.com/elastic/beats/packetbeat/protos" - "github.com/elastic/beats/packetbeat/protos/tcp" "github.com/tsg/gopacket" "github.com/tsg/gopacket/layers" @@ -43,18 +32,6 @@ const ( Response = true ) -// Notes that are added to messages during exceptional conditions. -const ( - NonDnsPacketMsg = "Packet's data could not be decoded as DNS." - NonDnsCompleteMsg = "Message's data could not be decoded as DNS." - NonDnsResponsePacketMsg = "Response packet's data could not be decoded as DNS." - EmptyMsg = "Message's data is empty." - DuplicateQueryMsg = "Another query with the same DNS ID from this client " + - "was received so this query was closed without receiving a response." - OrphanedResponseMsg = "Response was received without an associated query." - NoResponse = "No response to this query was received." -) - // Transport protocol. type Transport uint8 @@ -63,8 +40,6 @@ const ( TransportUdp ) -const DecodeOffset = 2 - var TransportNames = []string{ "tcp", "udp", @@ -159,33 +134,32 @@ func (t *DnsTuple) RevHashable() HashableDnsTuple { return t.revRaw } -// DnsMessage contains a single DNS message. -type DnsMessage struct { - Ts time.Time // Time when the message was received. - Tuple common.IpPortTuple // Source and destination addresses of packet. - CmdlineTuple *common.CmdlineTuple - Data *layers.DNS // Parsed DNS packet data. - Length int // Length of the DNS message in bytes. -} - -// DnsStream contains DNS data from one side of a TCP transmission. A pair -// of DnsStream's are used to represent the full conversation. -type DnsStream struct { - tcpTuple *common.TcpTuple - - data []byte +type Dns struct { + // Configuration data. + Ports []int + Send_request bool + Send_response bool + Include_authorities bool + Include_additionals bool - parseOffset int - bytesReceived int + // Cache of active DNS transactions. The map key is the HashableDnsTuple + // associated with the request. + transactions *common.Cache + transactionTimeout time.Duration - message *DnsMessage + results publisher.Client // Channel where results are pushed. } -// dnsPrivateData contains two DnsStream's that hold data from a complete TCP -// transmission. Element zero contains the response data. Element one contains -// the request data. -type dnsPrivateData struct { - Data [2]*DnsStream +// getTransaction returns the transaction associated with the given +// HashableDnsTuple. The lookup key should be the HashableDnsTuple associated +// with the request (src is the requestor). Nil is returned if the entry +// does not exist. +func (dns *Dns) getTransaction(k HashableDnsTuple) *DnsTransaction { + v := dns.transactions.Get(k) + if v != nil { + return v.(*DnsTransaction) + } + return nil } type DnsTransaction struct { @@ -220,34 +194,6 @@ func newTransaction(ts time.Time, tuple DnsTuple, cmd common.CmdlineTuple) *DnsT return trans } -type Dns struct { - // Configuration data. - Ports []int - Send_request bool - Send_response bool - Include_authorities bool - Include_additionals bool - - // Cache of active DNS transactions. The map key is the HashableDnsTuple - // associated with the request. - transactions *common.Cache - transactionTimeout time.Duration - - results publisher.Client // Channel where results are pushed. -} - -// getTransaction returns the transaction associated with the given -// HashableDnsTuple. The lookup key should be the HashableDnsTuple associated -// with the request (src is the requestor). Nil is returned if the entry -// does not exist. -func (dns *Dns) getTransaction(k HashableDnsTuple) *DnsTransaction { - v := dns.transactions.Get(k) - if v != nil { - return v.(*DnsTransaction) - } - return nil -} - // deleteTransaction deletes an entry from the transaction map and returns // the deleted element. If the key does not exist then nil is returned. func (dns *Dns) deleteTransaction(k HashableDnsTuple) *DnsTransaction { @@ -317,51 +263,19 @@ func (dns *Dns) GetPorts() []int { return dns.Ports } -func (dns *Dns) ParseUdp(pkt *protos.Packet) { - defer logp.Recover("Dns ParseUdp") - - logp.Debug("dns", "Parsing packet addressed with %s of length %d.", - pkt.Tuple.String(), len(pkt.Payload)) - - dnsPkt, err := decodeDnsData(TransportUdp, pkt.Payload) - if err != nil { - // This means that malformed requests or responses are being sent or - // that someone is attempting to the DNS port for non-DNS traffic. Both - // are issues that a monitoring system should report. - logp.Debug("dns", NonDnsPacketMsg+" addresses %s, length %d", - pkt.Tuple.String(), len(pkt.Payload)) - return - } - - dnsTuple := DnsTupleFromIpPort(&pkt.Tuple, TransportUdp, dnsPkt.ID) - dnsMsg := &DnsMessage{ - Ts: pkt.Ts, - Tuple: pkt.Tuple, - CmdlineTuple: procs.ProcWatcher.FindProcessesTuple(&pkt.Tuple), - Data: dnsPkt, - Length: len(pkt.Payload), - } - - if dnsMsg.Data.QR == Query { - dns.receivedDnsRequest(&dnsTuple, dnsMsg) - } else /* Response */ { - dns.receivedDnsResponse(&dnsTuple, dnsMsg) - } -} - func (dns *Dns) ConnectionTimeout() time.Duration { return dns.transactionTimeout } func (dns *Dns) receivedDnsRequest(tuple *DnsTuple, msg *DnsMessage) { - logp.Debug("dns", "Processing query. %s", tuple) + logp.Debug("dns", "Processing query. %s", tuple.String()) trans := dns.deleteTransaction(tuple.Hashable()) if trans != nil { // This happens if a client puts multiple requests in flight // with the same ID. - trans.Notes = append(trans.Notes, DuplicateQueryMsg) - logp.Debug("dns", DuplicateQueryMsg+" %s", tuple) + trans.Notes = append(trans.Notes, DuplicateQueryMsg.Error()) + logp.Debug("dns", "%s %s", DuplicateQueryMsg.Error(), tuple.String()) dns.publishTransaction(trans) dns.deleteTransaction(trans.tuple.Hashable()) } @@ -372,14 +286,14 @@ func (dns *Dns) receivedDnsRequest(tuple *DnsTuple, msg *DnsMessage) { } func (dns *Dns) receivedDnsResponse(tuple *DnsTuple, msg *DnsMessage) { - logp.Debug("dns", "Processing response. %s", tuple) + logp.Debug("dns", "Processing response. %s", tuple.String()) trans := dns.getTransaction(tuple.RevHashable()) if trans == nil { trans = newTransaction(msg.Ts, tuple.Reverse(), common.CmdlineTuple{ Src: msg.CmdlineTuple.Dst, Dst: msg.CmdlineTuple.Src}) - trans.Notes = append(trans.Notes, OrphanedResponseMsg) - logp.Debug("dns", OrphanedResponseMsg+" %s", tuple) + trans.Notes = append(trans.Notes, OrphanedResponse.Error()) + logp.Debug("dns", "%s %s", OrphanedResponse.Error(), tuple.String()) } trans.Response = msg @@ -463,8 +377,8 @@ func (dns *Dns) publishTransaction(t *DnsTransaction) { } func (dns *Dns) expireTransaction(t *DnsTransaction) { - t.Notes = append(t.Notes, NoResponse) - logp.Debug("dns", NoResponse+" %s", t.tuple.String()) + t.Notes = append(t.Notes, NoResponse.Error()) + logp.Debug("dns", "%s %s", NoResponse.Error(), t.tuple.String()) dns.publishTransaction(t) } @@ -702,7 +616,7 @@ func nameToString(name []byte) string { // decodeDnsData decodes a byte array into a DNS struct. If an error occurs // then the returnd dns pointer will be nil. This method recovers from panics // and is concurrency-safe. -func decodeDnsData(transport Transport, data []byte) (dns *layers.DNS, err error) { +func decodeDnsData(transport Transport, rawData []byte) (dns *layers.DNS, err error) { var offset int if transport == TransportTcp { offset = DecodeOffset @@ -716,187 +630,9 @@ func decodeDnsData(transport Transport, data []byte) (dns *layers.DNS, err error }() d := &layers.DNS{} - err = d.DecodeFromBytes(data[offset:], gopacket.NilDecodeFeedback) + err = d.DecodeFromBytes(rawData[offset:], gopacket.NilDecodeFeedback) if err != nil { - return nil, err + return nil, NonDnsMsg } return d, nil } - -// TCP implementation - -func (dns *Dns) Parse(pkt *protos.Packet, tcpTuple *common.TcpTuple, dir uint8, private protos.ProtocolData) protos.ProtocolData { - defer logp.Recover("DNS ParseTcp") - - logp.Debug("dns", "Parsing packet addressed with %s of length %d.", - pkt.Tuple.String(), len(pkt.Payload)) - - priv := dnsPrivateData{} - - if private != nil { - var ok bool - priv, ok = private.(dnsPrivateData) - if !ok { - priv = dnsPrivateData{} - } - } - - payload := pkt.Payload - - stream := &priv.Data[dir] - - if *stream == nil { - *stream = &DnsStream{ - tcpTuple: tcpTuple, - data: payload, - message: &DnsMessage{Ts: pkt.Ts, Tuple: pkt.Tuple}, - } - if len(payload) <= DecodeOffset { - logp.Debug("dns", EmptyMsg+" addresses %s", - tcpTuple.String()) - - return priv - } - } else { - (*stream).data = append((*stream).data, payload...) - dataLength := len((*stream).data) - if dataLength > tcp.TCP_MAX_DATA_IN_STREAM { - logp.Debug("dns", "Stream data too large, dropping DNS stream") - return priv - } - if dataLength <= DecodeOffset { - logp.Debug("dns", EmptyMsg+" addresses %s", - tcpTuple.String()) - return priv - } - } - - data, err := decodeDnsData(TransportTcp, (*stream).data) - - if err != nil { - logp.Debug("dns", NonDnsCompleteMsg+" addresses %s, length %d", - tcpTuple.String(), len((*stream).data)) - - // wait for decoding with the next segment - return priv - } - - dns.messageComplete(tcpTuple, dir, *stream, data) - return priv -} - -func (dns *Dns) messageComplete(tcpTuple *common.TcpTuple, dir uint8, s *DnsStream, decodedData *layers.DNS) { - dns.handleDns(s.message, tcpTuple, dir, s.data, decodedData) - - s.PrepareForNewMessage() -} - -func (dns *Dns) handleDns(m *DnsMessage, tcpTuple *common.TcpTuple, dir uint8, data []byte, decodedData *layers.DNS) { - dnsTuple := DnsTupleFromIpPort(&m.Tuple, TransportTcp, decodedData.ID) - m.CmdlineTuple = procs.ProcWatcher.FindProcessesTuple(tcpTuple.IpPort()) - m.Data = decodedData - m.Length = len(data) - - if decodedData.QR == Query { - dns.receivedDnsRequest(&dnsTuple, m) - } else /* Response */ { - dns.receivedDnsResponse(&dnsTuple, m) - } -} - -func (stream *DnsStream) PrepareForNewMessage() { - stream.message = nil -} - -func (dns *Dns) ReceivedFin(tcpTuple *common.TcpTuple, dir uint8, private protos.ProtocolData) protos.ProtocolData { - if private == nil { - return private - } - dnsData, ok := private.(dnsPrivateData) - if !ok { - return private - } - if dnsData.Data[dir] == nil { - return dnsData - } - stream := dnsData.Data[dir] - if stream.message != nil { - decodedData, err := decodeDnsData(TransportTcp, stream.data) - - if err == nil { - dns.messageComplete(tcpTuple, dir, stream, decodedData) - } else /*Failed decode */ { - if dir == tcp.TcpDirectionReverse { - dns.publishDecodeFailureNotes(dnsData) - stream.PrepareForNewMessage() - } - logp.Debug("dns", NonDnsCompleteMsg+" addresses %s, length %d", - tcpTuple.String(), len(stream.data)) - } - } - - return dnsData -} - -func (dns *Dns) GapInStream(tcpTuple *common.TcpTuple, dir uint8, nbytes int, private protos.ProtocolData) (priv protos.ProtocolData, drop bool) { - dnsData, ok := private.(dnsPrivateData) - - if !ok { - return private, false - } - - stream := dnsData.Data[dir] - - if stream == nil || stream.message == nil { - return private, false - } - - decodedData, err := decodeDnsData(TransportTcp, stream.data) - - // Add Notes if the failed stream is the response - if err != nil { - if dir == tcp.TcpDirectionReverse { - dns.publishDecodeFailureNotes(dnsData) - } - - // drop the stream because it is binary and it would be rare to have a decodable message later - logp.Debug("dns", NonDnsCompleteMsg+" addresses %s, length %d", - tcpTuple.String(), len(stream.data)) - return private, true - } - - // publish and ignore the gap. No case should reach this code though ... - dns.messageComplete(tcpTuple, dir, stream, decodedData) - return private, false -} - -// Add Notes to the query stream about a failure to decode the response -func (dns *Dns) publishDecodeFailureNotes(dnsData dnsPrivateData) { - streamOrigin := dnsData.Data[tcp.TcpDirectionOriginal] - streamReverse := dnsData.Data[tcp.TcpDirectionReverse] - - if streamOrigin == nil || streamReverse == nil { - return - } - - dataOrigin, err := decodeDnsData(TransportTcp, streamOrigin.data) - tupleReverse := streamReverse.message.Tuple - - if err == nil { - dnsTupleReverse := DnsTupleFromIpPort(&tupleReverse, TransportTcp, dataOrigin.ID) - hashDnsTupleOrigin := (&dnsTupleReverse).RevHashable() - - trans := dns.deleteTransaction(hashDnsTupleOrigin) - - if trans == nil { // happens when a Gap is followed by Fin - return - } - - trans.Notes = append(trans.Notes, NonDnsResponsePacketMsg) - - dns.publishTransaction(trans) - dns.deleteTransaction(hashDnsTupleOrigin) - } else { - logp.Debug("dns", "Unabled to decode response with adresses %s has no associated query", streamReverse.tcpTuple.String()) - } -} diff --git a/packetbeat/protos/dns/dns_tcp.go b/packetbeat/protos/dns/dns_tcp.go new file mode 100644 index 00000000000..55bb4048e10 --- /dev/null +++ b/packetbeat/protos/dns/dns_tcp.go @@ -0,0 +1,302 @@ +package dns + +import ( + "encoding/binary" + "time" + + "github.com/elastic/beats/libbeat/common" + "github.com/elastic/beats/libbeat/logp" + + "github.com/elastic/beats/packetbeat/procs" + "github.com/elastic/beats/packetbeat/protos" + "github.com/elastic/beats/packetbeat/protos/tcp" + + "github.com/tsg/gopacket/layers" +) + +const MaxDnsMessageSize = (1 << 16) - 1 + +// RFC 1035 +// The 2 first bytes contain the length of the message +const DecodeOffset = 2 + +// DnsMessage contains a single DNS message. +type DnsMessage struct { + Ts time.Time // Time when the message was received. + Tuple common.IpPortTuple // Source and destination addresses of packet. + CmdlineTuple *common.CmdlineTuple + Data *layers.DNS // Parsed DNS packet data. + Length int // Length of the DNS message in bytes (without DecodeOffset). +} + +// DnsStream contains DNS data from one side of a TCP transmission. A pair +// of DnsStream's are used to represent the full conversation. +type DnsStream struct { + tcpTuple *common.TcpTuple + rawData []byte + parseOffset int + message *DnsMessage +} + +// dnsConnectionData contains two DnsStream's that hold data from a complete TCP +// transmission. Element zero contains the response data. Element one contains +// the request data. +// prevRequest (previous Request) is used to add Notes to a transaction when a failing answer is encountered +type dnsConnectionData struct { + Data [2]*DnsStream + prevRequest *DnsMessage +} + +func (dns *Dns) Parse(pkt *protos.Packet, tcpTuple *common.TcpTuple, dir uint8, private protos.ProtocolData) protos.ProtocolData { + defer logp.Recover("Dns ParseTcp") + + logp.Debug("dns", "Parsing packet addressed with %s of length %d.", + pkt.Tuple.String(), len(pkt.Payload)) + + conn := ensureDnsConnection(private) + + conn = dns.doParse(conn, pkt, tcpTuple, dir) + if conn == nil { + return nil + } + + return conn +} + +func ensureDnsConnection(private protos.ProtocolData) *dnsConnectionData { + if private == nil { + return &dnsConnectionData{} + } + + conn, ok := private.(*dnsConnectionData) + if !ok { + logp.Warn("Dns connection data type error, create new one") + return &dnsConnectionData{} + } + if conn == nil { + logp.Warn("Unexpected: dns connection data not set, create new one") + return &dnsConnectionData{} + } + + return conn +} + +func (dns *Dns) doParse(conn *dnsConnectionData, pkt *protos.Packet, tcpTuple *common.TcpTuple, dir uint8) *dnsConnectionData { + stream := conn.Data[dir] + payload := pkt.Payload + + if stream == nil { + stream = newStream(pkt, tcpTuple) + conn.Data[dir] = stream + } else { + if stream.message == nil { // nth message of the same stream + stream.message = &DnsMessage{Ts: pkt.Ts, Tuple: pkt.Tuple} + } + + stream.rawData = append(stream.rawData, payload...) + if len(stream.rawData) > tcp.TCP_MAX_DATA_IN_STREAM { + logp.Debug("dns", "Stream data too large, dropping DNS stream") + conn.Data[dir] = nil + return conn + } + } + decodedData, err := stream.handleTcpRawData() + + if err != nil { + + if err == IncompleteMsg { + logp.Debug("dns", "Waiting for more raw data") + return conn + } + + if dir == tcp.TcpDirectionReverse { + dns.publishResponseError(conn, err) + } + + logp.Debug("dns", "%s addresses %s, length %d", err.Error(), + tcpTuple.String(), len(stream.rawData)) + + // This means that malformed requests or responses are being sent... + // TODO: publish the situation also if Request + conn.Data[dir] = nil + return conn + } + + dns.messageComplete(conn, tcpTuple, dir, decodedData) + stream.PrepareForNewMessage() + return conn +} + +func newStream(pkt *protos.Packet, tcpTuple *common.TcpTuple) *DnsStream { + return &DnsStream{ + tcpTuple: tcpTuple, + rawData: pkt.Payload, + message: &DnsMessage{Ts: pkt.Ts, Tuple: pkt.Tuple}, + } +} + +func (dns *Dns) messageComplete(conn *dnsConnectionData, tcpTuple *common.TcpTuple, dir uint8, decodedData *layers.DNS) { + dns.handleDns(conn, tcpTuple, decodedData, dir) +} + +func (dns *Dns) handleDns(conn *dnsConnectionData, tcpTuple *common.TcpTuple, decodedData *layers.DNS, dir uint8) { + message := conn.Data[dir].message + dnsTuple := DnsTupleFromIpPort(&message.Tuple, TransportTcp, decodedData.ID) + + message.CmdlineTuple = procs.ProcWatcher.FindProcessesTuple(tcpTuple.IpPort()) + message.Data = decodedData + message.Length += DecodeOffset + + if decodedData.QR == Query { + dns.receivedDnsRequest(&dnsTuple, message) + conn.prevRequest = message + } else /* Response */ { + dns.receivedDnsResponse(&dnsTuple, message) + conn.prevRequest = nil + } +} + +func (stream *DnsStream) PrepareForNewMessage() { + stream.rawData = stream.rawData[stream.parseOffset:] + stream.message = nil + stream.parseOffset = 0 +} + +func (dns *Dns) ReceivedFin(tcpTuple *common.TcpTuple, dir uint8, private protos.ProtocolData) protos.ProtocolData { + if private == nil { + return nil + } + conn, ok := private.(*dnsConnectionData) + if !ok { + return private + } + stream := conn.Data[dir] + + if stream == nil || stream.message == nil { + return conn + } + + decodedData, err := stream.handleTcpRawData() + + if err == nil { + dns.messageComplete(conn, tcpTuple, dir, decodedData) + return conn + } + + if dir == tcp.TcpDirectionReverse { + dns.publishResponseError(conn, err) + } + + logp.Debug("dns", "%s addresses %s, length %d", err.Error(), + tcpTuple.String(), len(stream.rawData)) + + return conn +} + +func (dns *Dns) GapInStream(tcpTuple *common.TcpTuple, dir uint8, nbytes int, private protos.ProtocolData) (priv protos.ProtocolData, drop bool) { + if private == nil { + return private, true + } + conn, ok := private.(*dnsConnectionData) + if !ok { + return private, false + } + stream := conn.Data[dir] + + if stream == nil || stream.message == nil { + return private, false + } + + decodedData, err := stream.handleTcpRawData() + + if err == nil { + dns.messageComplete(conn, tcpTuple, dir, decodedData) + return private, true + } + + if dir == tcp.TcpDirectionReverse { + dns.publishResponseError(conn, err) + } + + logp.Debug("dns", "%s addresses %s, length %d", err.Error(), + tcpTuple.String(), len(stream.rawData)) + logp.Debug("dns", "Dropping the stream %s", tcpTuple.String()) + + // drop the stream because it is binary Data and it would be unexpected to have a decodable message later + return private, true +} + +// Add Notes to the transaction about a failure in the response +// Publish and remove the transaction +func (dns *Dns) publishResponseError(conn *dnsConnectionData, err error) { + streamOrigin := conn.Data[tcp.TcpDirectionOriginal] + streamReverse := conn.Data[tcp.TcpDirectionReverse] + + if streamOrigin == nil || conn.prevRequest == nil || streamReverse == nil { + return + } + + dataOrigin := conn.prevRequest.Data + dnsTupleOrigin := DnsTupleFromIpPort(&conn.prevRequest.Tuple, TransportTcp, dataOrigin.ID) + hashDnsTupleOrigin := (&dnsTupleOrigin).Hashable() + + trans := dns.deleteTransaction(hashDnsTupleOrigin) + + if trans == nil { // happens if Parse, Gap or Fin already published the response error + return + } + + errDns, ok := err.(*DNSError) + if !ok { + return + } + trans.Notes = append(trans.Notes, errDns.ResponseError()) + + // Should we publish the length (bytes_out) of the failed Response? + //streamReverse.message.Length = len(streamReverse.rawData) + //trans.Response = streamReverse.message + + dns.publishTransaction(trans) + dns.deleteTransaction(hashDnsTupleOrigin) +} + +// Manages data length prior to decoding the data and manages errors after decoding +func (stream *DnsStream) handleTcpRawData() (*layers.DNS, error) { + rawData := stream.rawData + messageLength := len(rawData) + + if messageLength < DecodeOffset { + return nil, IncompleteMsg + } + + if stream.message.Length == 0 { + stream.message.Length = int(binary.BigEndian.Uint16(rawData[:DecodeOffset])) + stream.parseOffset = stream.message.Length + DecodeOffset + + if stream.message.Length <= 0 { + // TODO: This means that malformed requests or responses are being sent or + // that someone is attempting to the DNS port for non-DNS traffic. + // We might want to publish this in the future, for security reasons + return nil, ZeroLengthMsg + } + } + + if stream.message.Length > MaxDnsMessageSize { // Should never be true though ... + // TODO: This means that malformed requests or responses are being sent or + // that someone is attempting to the DNS port for non-DNS traffic. Both + // are issues that a monitoring system should report. + return nil, UnexpectedLengthMsg + } + + if messageLength < stream.parseOffset { + return nil, IncompleteMsg + } + + decodedData, err := decodeDnsData(TransportTcp, rawData[:stream.parseOffset]) + + if err != nil { + return nil, err + } + + return decodedData, nil +} diff --git a/packetbeat/protos/dns/dns_tcp_test.go b/packetbeat/protos/dns/dns_tcp_test.go new file mode 100644 index 00000000000..db12324762e --- /dev/null +++ b/packetbeat/protos/dns/dns_tcp_test.go @@ -0,0 +1,648 @@ +// Unit tests and benchmarks for the dns package. +// +// The byte array test data was generated from pcap files using the gopacket +// test_creator.py script contained in the gopacket repository. The script was +// modified to drop the Ethernet, IP, and UDP headers from the byte arrays +// (skip the first 54 bytes). + +package dns + +import ( + "fmt" + "math/rand" + "net" + "testing" + + "github.com/elastic/beats/packetbeat/protos" + "github.com/elastic/beats/packetbeat/protos/tcp" + + "github.com/elastic/beats/libbeat/common" + "github.com/elastic/beats/libbeat/publisher" + + "github.com/stretchr/testify/assert" +) + +// Verify that the interface TCP has been satisfied. +var _ protos.TcpProtocolPlugin = &Dns{} + +var ( + messagesTcp = []DnsTestMessage{ + elasticATcp, + zoneAxfrTcp, + githubPtrTcp, + sophosTxtTcp, + } + + elasticATcp = DnsTestMessage{ + id: 11674, + opcode: "QUERY", + flags: []string{"rd", "ra"}, + rcode: "NOERROR", + q_class: "IN", + q_type: "A", + q_name: "elastic.co", + answers: []string{"54.201.204.244", "54.200.185.88"}, + authorities: []string{"NS-835.AWSDNS-40.NET", "NS-1183.AWSDNS-19.ORG", "NS-2007.AWSDNS-58.CO.UK", "NS-66.AWSDNS-08.COM"}, + request: []byte{ + 0x00, 0x1c, 0x2d, 0x9a, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x65, + 0x6c, 0x61, 0x73, 0x74, 0x69, 0x63, 0x02, 0x63, 0x6f, 0x00, 0x00, 0x01, 0x00, 0x01, + }, + response: []byte{ + 0x00, 0xc7, 0x2d, 0x9a, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x07, 0x65, + 0x6c, 0x61, 0x73, 0x74, 0x69, 0x63, 0x02, 0x63, 0x6f, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x04, 0x36, 0xc8, 0xb9, 0x58, 0xc0, 0x0c, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x04, 0x36, 0xc9, 0xcc, 0xf4, 0xc0, 0x0c, + 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x16, 0x82, 0x00, 0x16, 0x06, 0x4e, 0x53, 0x2d, 0x38, 0x33, + 0x35, 0x09, 0x41, 0x57, 0x53, 0x44, 0x4e, 0x53, 0x2d, 0x34, 0x30, 0x03, 0x4e, 0x45, 0x54, 0x00, + 0xc0, 0x0c, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x16, 0x82, 0x00, 0x17, 0x07, 0x4e, 0x53, 0x2d, + 0x31, 0x31, 0x38, 0x33, 0x09, 0x41, 0x57, 0x53, 0x44, 0x4e, 0x53, 0x2d, 0x31, 0x39, 0x03, 0x4f, + 0x52, 0x47, 0x00, 0xc0, 0x0c, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x16, 0x82, 0x00, 0x19, 0x07, + 0x4e, 0x53, 0x2d, 0x32, 0x30, 0x30, 0x37, 0x09, 0x41, 0x57, 0x53, 0x44, 0x4e, 0x53, 0x2d, 0x35, + 0x38, 0x02, 0x43, 0x4f, 0x02, 0x55, 0x4b, 0x00, 0xc0, 0x0c, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, + 0x16, 0x82, 0x00, 0x15, 0x05, 0x4e, 0x53, 0x2d, 0x36, 0x36, 0x09, 0x41, 0x57, 0x53, 0x44, 0x4e, + 0x53, 0x2d, 0x30, 0x38, 0x03, 0x43, 0x4f, 0x4d, 0x00, + }, + } + + zoneAxfrTcp = DnsTestMessage{ + id: 0, + opcode: "QUERY", + rcode: "NOERROR", + q_class: "IN", + q_type: "AXFR", + q_name: "etas.com", + answers: []string{"training2003p", "training2003p", "1.1.1.1", "training2003p"}, + request: []byte{ + 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x65, + 0x74, 0x61, 0x73, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0xfc, 0x00, 0x01, 0x4d, 0x53, + }, + response: []byte{ + 0x00, 0xc3, 0x00, 0x00, 0x80, 0x80, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x04, 0x65, + 0x74, 0x61, 0x73, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0xfc, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x06, + 0x00, 0x01, 0x00, 0x00, 0x0e, 0x10, 0x00, 0x2f, 0x0d, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, + 0x67, 0x32, 0x30, 0x30, 0x33, 0x70, 0x00, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x6d, 0x61, 0x73, 0x74, + 0x65, 0x72, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x02, 0x58, 0x00, + 0x01, 0x51, 0x80, 0x00, 0x00, 0x0e, 0x10, 0xc0, 0x0c, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x0e, + 0x10, 0x00, 0x0f, 0x0d, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x32, 0x30, 0x30, 0x33, + 0x70, 0x00, 0x07, 0x77, 0x65, 0x6c, 0x63, 0x6f, 0x6d, 0x65, 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x00, 0x0e, 0x10, 0x00, 0x04, 0x01, 0x01, 0x01, 0x01, 0xc0, 0x0c, 0x00, 0x06, 0x00, 0x01, + 0x00, 0x00, 0x0e, 0x10, 0x00, 0x2f, 0x0d, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x32, + 0x30, 0x30, 0x33, 0x70, 0x00, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x02, 0x58, 0x00, 0x01, 0x51, + 0x80, 0x00, 0x00, 0x0e, 0x10, + }, + } + + githubPtrTcp = DnsTestMessage{ + id: 6766, + opcode: "QUERY", + flags: []string{"rd", "ra"}, + rcode: "NOERROR", + q_class: "IN", + q_type: "PTR", + q_name: "131.252.30.192.in-addr.arpa", + answers: []string{"github.com"}, + authorities: []string{"ns1.p16.dynect.net", "ns3.p16.dynect.net", "ns4.p16.dynect.net", "ns2.p16.dynect.net"}, + request: []byte{ + 0x00, 0x2d, 0x1a, 0x6e, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x31, + 0x33, 0x31, 0x03, 0x32, 0x35, 0x32, 0x02, 0x33, 0x30, 0x03, 0x31, 0x39, 0x32, 0x07, 0x69, 0x6e, + 0x2d, 0x61, 0x64, 0x64, 0x72, 0x04, 0x61, 0x72, 0x70, 0x61, 0x00, 0x00, 0x0c, 0x00, 0x01, + }, + response: []byte{ + 0x00, 0x9b, 0x1a, 0x6e, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x03, 0x31, + 0x33, 0x31, 0x03, 0x32, 0x35, 0x32, 0x02, 0x33, 0x30, 0x03, 0x31, 0x39, 0x32, 0x07, 0x69, 0x6e, + 0x2d, 0x61, 0x64, 0x64, 0x72, 0x04, 0x61, 0x72, 0x70, 0x61, 0x00, 0x00, 0x0c, 0x00, 0x01, 0xc0, + 0x0c, 0x00, 0x0c, 0x00, 0x01, 0x00, 0x00, 0x0e, 0x07, 0x00, 0x0c, 0x06, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0xc0, 0x10, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x51, + 0x77, 0x00, 0x14, 0x03, 0x6e, 0x73, 0x31, 0x03, 0x70, 0x31, 0x36, 0x06, 0x64, 0x79, 0x6e, 0x65, + 0x63, 0x74, 0x03, 0x6e, 0x65, 0x74, 0x00, 0xc0, 0x10, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x51, + 0x77, 0x00, 0x06, 0x03, 0x6e, 0x73, 0x33, 0xc0, 0x55, 0xc0, 0x10, 0x00, 0x02, 0x00, 0x01, 0x00, + 0x01, 0x51, 0x77, 0x00, 0x06, 0x03, 0x6e, 0x73, 0x34, 0xc0, 0x55, 0xc0, 0x10, 0x00, 0x02, 0x00, + 0x01, 0x00, 0x01, 0x51, 0x77, 0x00, 0x06, 0x03, 0x6e, 0x73, 0x32, 0xc0, 0x55, + }, + } + + sophosTxtTcp = DnsTestMessage{ + id: 35009, + opcode: "QUERY", + flags: []string{"rd", "ra"}, + rcode: "NXDOMAIN", + q_class: "IN", + q_type: "TXT", + q_name: "3.1o19ss00s2s17s4qp375sp49r830n2n4n923s8839052s7p7768s53365226pp3.659p1r741os37393" + + "648s2348o762q1066q53rq5p4614r1q4781qpr16n809qp4.879o3o734q9sns005o3pp76q83.2q65qns3spns" + + "1081s5rn5sr74opqrqnpq6rn3ro5.i.00.mac.sophosxl.net", + request: []byte{ + 0x00, 0xed, 0x88, 0xc1, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x33, + 0x3f, 0x31, 0x6f, 0x31, 0x39, 0x73, 0x73, 0x30, 0x30, 0x73, 0x32, 0x73, 0x31, 0x37, 0x73, 0x34, + 0x71, 0x70, 0x33, 0x37, 0x35, 0x73, 0x70, 0x34, 0x39, 0x72, 0x38, 0x33, 0x30, 0x6e, 0x32, 0x6e, + 0x34, 0x6e, 0x39, 0x32, 0x33, 0x73, 0x38, 0x38, 0x33, 0x39, 0x30, 0x35, 0x32, 0x73, 0x37, 0x70, + 0x37, 0x37, 0x36, 0x38, 0x73, 0x35, 0x33, 0x33, 0x36, 0x35, 0x32, 0x32, 0x36, 0x70, 0x70, 0x33, + 0x3f, 0x36, 0x35, 0x39, 0x70, 0x31, 0x72, 0x37, 0x34, 0x31, 0x6f, 0x73, 0x33, 0x37, 0x33, 0x39, + 0x33, 0x36, 0x34, 0x38, 0x73, 0x32, 0x33, 0x34, 0x38, 0x6f, 0x37, 0x36, 0x32, 0x71, 0x31, 0x30, + 0x36, 0x36, 0x71, 0x35, 0x33, 0x72, 0x71, 0x35, 0x70, 0x34, 0x36, 0x31, 0x34, 0x72, 0x31, 0x71, + 0x34, 0x37, 0x38, 0x31, 0x71, 0x70, 0x72, 0x31, 0x36, 0x6e, 0x38, 0x30, 0x39, 0x71, 0x70, 0x34, + 0x1a, 0x38, 0x37, 0x39, 0x6f, 0x33, 0x6f, 0x37, 0x33, 0x34, 0x71, 0x39, 0x73, 0x6e, 0x73, 0x30, + 0x30, 0x35, 0x6f, 0x33, 0x70, 0x70, 0x37, 0x36, 0x71, 0x38, 0x33, 0x28, 0x32, 0x71, 0x36, 0x35, + 0x71, 0x6e, 0x73, 0x33, 0x73, 0x70, 0x6e, 0x73, 0x31, 0x30, 0x38, 0x31, 0x73, 0x35, 0x72, 0x6e, + 0x35, 0x73, 0x72, 0x37, 0x34, 0x6f, 0x70, 0x71, 0x72, 0x71, 0x6e, 0x70, 0x71, 0x36, 0x72, 0x6e, + 0x33, 0x72, 0x6f, 0x35, 0x01, 0x69, 0x02, 0x30, 0x30, 0x03, 0x6d, 0x61, 0x63, 0x08, 0x73, 0x6f, + 0x70, 0x68, 0x6f, 0x73, 0x78, 0x6c, 0x03, 0x6e, 0x65, 0x74, 0x00, 0x00, 0x10, 0x00, 0x01, + }, + response: []byte{ + 0x00, 0xed, 0x88, 0xc1, 0x81, 0x83, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x33, + 0x3f, 0x31, 0x6f, 0x31, 0x39, 0x73, 0x73, 0x30, 0x30, 0x73, 0x32, 0x73, 0x31, 0x37, 0x73, 0x34, + 0x71, 0x70, 0x33, 0x37, 0x35, 0x73, 0x70, 0x34, 0x39, 0x72, 0x38, 0x33, 0x30, 0x6e, 0x32, 0x6e, + 0x34, 0x6e, 0x39, 0x32, 0x33, 0x73, 0x38, 0x38, 0x33, 0x39, 0x30, 0x35, 0x32, 0x73, 0x37, 0x70, + 0x37, 0x37, 0x36, 0x38, 0x73, 0x35, 0x33, 0x33, 0x36, 0x35, 0x32, 0x32, 0x36, 0x70, 0x70, 0x33, + 0x3f, 0x36, 0x35, 0x39, 0x70, 0x31, 0x72, 0x37, 0x34, 0x31, 0x6f, 0x73, 0x33, 0x37, 0x33, 0x39, + 0x33, 0x36, 0x34, 0x38, 0x73, 0x32, 0x33, 0x34, 0x38, 0x6f, 0x37, 0x36, 0x32, 0x71, 0x31, 0x30, + 0x36, 0x36, 0x71, 0x35, 0x33, 0x72, 0x71, 0x35, 0x70, 0x34, 0x36, 0x31, 0x34, 0x72, 0x31, 0x71, + 0x34, 0x37, 0x38, 0x31, 0x71, 0x70, 0x72, 0x31, 0x36, 0x6e, 0x38, 0x30, 0x39, 0x71, 0x70, 0x34, + 0x1a, 0x38, 0x37, 0x39, 0x6f, 0x33, 0x6f, 0x37, 0x33, 0x34, 0x71, 0x39, 0x73, 0x6e, 0x73, 0x30, + 0x30, 0x35, 0x6f, 0x33, 0x70, 0x70, 0x37, 0x36, 0x71, 0x38, 0x33, 0x28, 0x32, 0x71, 0x36, 0x35, + 0x71, 0x6e, 0x73, 0x33, 0x73, 0x70, 0x6e, 0x73, 0x31, 0x30, 0x38, 0x31, 0x73, 0x35, 0x72, 0x6e, + 0x35, 0x73, 0x72, 0x37, 0x34, 0x6f, 0x70, 0x71, 0x72, 0x71, 0x6e, 0x70, 0x71, 0x36, 0x72, 0x6e, + 0x33, 0x72, 0x6f, 0x35, 0x01, 0x69, 0x02, 0x30, 0x30, 0x03, 0x6d, 0x61, 0x63, 0x08, 0x73, 0x6f, + 0x70, 0x68, 0x6f, 0x73, 0x78, 0x6c, 0x03, 0x6e, 0x65, 0x74, 0x00, 0x00, 0x10, 0x00, 0x01, + }, + } +) + +func testTcpTuple() *common.TcpTuple { + t := &common.TcpTuple{ + Ip_length: 4, + Src_ip: net.IPv4(192, 168, 0, 1), Dst_ip: net.IPv4(192, 168, 0, 2), + Src_port: ClientPort, Dst_port: ServerPort, + } + t.ComputeHashebles() + return t +} + +func TestDecodeTcp_nonDnsMsgRequest(t *testing.T) { + rawData := []byte{0, 2, 1, 2} + + _, err := decodeDnsData(TransportTcp, rawData) + assert.Equal(t, err, NonDnsMsg) +} + +// Verify that the split lone request packet is decoded. +func TestDecodeTcp_splitRequest(t *testing.T) { + stream := &DnsStream{rawData: sophosTxtTcp.request[:10], message: new(DnsMessage)} + _, err := decodeDnsData(TransportTcp, stream.rawData) + + assert.NotNil(t, err, "Not expecting a complete message yet") + + stream.rawData = append(stream.rawData, sophosTxtTcp.request[10:]...) + _, err = decodeDnsData(TransportTcp, stream.rawData) + + assert.Nil(t, err, "Message should be complete") +} + +func TestParseTcp_errorNonDnsMsgResponse(t *testing.T) { + var private protos.ProtocolData + dns := newDns(testing.Verbose()) + tcptuple := testTcpTuple() + q := elasticATcp + packet := newPacket(forward, q.request) + + private = dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + + r := []byte{0, 2, 1, 2} + packet = newPacket(reverse, r) + dns.Parse(packet, tcptuple, tcp.TcpDirectionReverse, private) + assert.Empty(t, dns.transactions.Size(), "There should be no transaction.") + + m := expectResult(t, dns) + assertRequest(t, m, q) + assert.Equal(t, "tcp", mapValue(t, m, "transport")) + assert.Equal(t, len(q.request), mapValue(t, m, "bytes_in")) + assert.Nil(t, mapValue(t, m, "bytes_out")) + assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) + assert.Equal(t, NonDnsMsg.ResponseError(), mapValue(t, m, "notes")) +} + +// Verify that a request message with length (first two bytes value) of zero is not published +func TestParseTcp_zeroLengthMsgRequest(t *testing.T) { + var private protos.ProtocolData + dns := newDns(testing.Verbose()) + tcptuple := testTcpTuple() + packet := newPacket(forward, []byte{0, 0, 1, 2}) + + dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + assert.Empty(t, dns.transactions.Size(), "There should be no transactions.") + client := dns.results.(publisher.ChanClient) + close(client.Channel) + assert.Nil(t, <-client.Channel, "No result should have been published.") +} + +// Verify that a response message with length (first two bytes value) of zero is published with the corresponding Notes +func TestParseTcp_errorZeroLengthMsgResponse(t *testing.T) { + var private protos.ProtocolData + dns := newDns(testing.Verbose()) + tcptuple := testTcpTuple() + q := elasticATcp + packet := newPacket(forward, q.request) + + private = dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + + r := []byte{0, 0, 1, 2} + packet = newPacket(reverse, r) + dns.Parse(packet, tcptuple, tcp.TcpDirectionReverse, private) + assert.Empty(t, dns.transactions.Size(), "There should be no transaction.") + + m := expectResult(t, dns) + assertRequest(t, m, q) + assert.Equal(t, "tcp", mapValue(t, m, "transport")) + assert.Equal(t, len(q.request), mapValue(t, m, "bytes_in")) + assert.Nil(t, mapValue(t, m, "bytes_out")) + assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) + assert.Equal(t, ZeroLengthMsg.ResponseError(), mapValue(t, m, "notes")) +} + +// Verify that an empty packet is safely handled (no panics). +func TestParseTcp_emptyPacket(t *testing.T) { + var private protos.ProtocolData + dns := newDns(testing.Verbose()) + packet := newPacket(forward, []byte{}) + tcptuple := testTcpTuple() + + dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + assert.Empty(t, dns.transactions.Size(), "There should be no transactions.") + client := dns.results.(publisher.ChanClient) + close(client.Channel) + assert.Nil(t, <-client.Channel, "No result should have been published.") +} + +// Verify that a malformed packet is safely handled (no panics). +func TestParseTcp_malformedPacket(t *testing.T) { + var private protos.ProtocolData + dns := newDns(testing.Verbose()) + garbage := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13} + tcptuple := testTcpTuple() + packet := newPacket(forward, garbage) + + dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + assert.Empty(t, dns.transactions.Size(), "There should be no transactions.") +} + +// Verify that the lone request packet is parsed. +func TestParseTcp_requestPacket(t *testing.T) { + var private protos.ProtocolData + dns := newDns(testing.Verbose()) + packet := newPacket(forward, elasticATcp.request) + tcptuple := testTcpTuple() + + dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + client := dns.results.(publisher.ChanClient) + close(client.Channel) + assert.Nil(t, <-client.Channel, "No result should have been published.") +} + +// Verify that the lone response packet is parsed and that an error +// result is published. +func TestParseTcp_errorResponseOnly(t *testing.T) { + var private protos.ProtocolData + dns := newDns(testing.Verbose()) + q := elasticATcp + packet := newPacket(reverse, q.response) + tcptuple := testTcpTuple() + + dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + m := expectResult(t, dns) + assert.Equal(t, "tcp", mapValue(t, m, "transport")) + assert.Nil(t, mapValue(t, m, "bytes_in")) + assert.Equal(t, len(q.response), mapValue(t, m, "bytes_out")) + assert.Nil(t, mapValue(t, m, "responsetime")) + assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) + assert.Equal(t, OrphanedResponse.Error(), mapValue(t, m, "notes")) + assertMapStrData(t, m, q) +} + +// Verify that the first request is published without a response and that +// the status is error. This second packet will remain in the transaction +// map awaiting a response. +func TestParseTcp_errorDuplicateRequests(t *testing.T) { + var private protos.ProtocolData + dns := newDns(testing.Verbose()) + q := elasticATcp + packet := newPacket(forward, q.request) + tcptuple := testTcpTuple() + + dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + + dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + // The first request is published and this one becomes a transaction + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + + m := expectResult(t, dns) + assertRequest(t, m, q) + assert.Equal(t, "tcp", mapValue(t, m, "transport")) + assert.Equal(t, len(q.request), mapValue(t, m, "bytes_in")) + assert.Nil(t, mapValue(t, m, "bytes_out")) + assert.Nil(t, mapValue(t, m, "responsetime")) + assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) + assert.Equal(t, DuplicateQueryMsg.Error(), mapValue(t, m, "notes")) +} + +// Same than the previous one but on the same stream +// Checks that PrepareNewMessage and Parse can manage two messages on the same stream, in different packets +func TestParseTcp_errorDuplicateRequestsOneStream(t *testing.T) { + var private protos.ProtocolData + dns := newDns(testing.Verbose()) + q := elasticATcp + packet := newPacket(forward, q.request) + tcptuple := testTcpTuple() + + private = dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + + dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + // The first query is published and this one becomes a transaction + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + + m := expectResult(t, dns) + assertRequest(t, m, q) + assert.Equal(t, "tcp", mapValue(t, m, "transport")) + assert.Equal(t, len(q.request), mapValue(t, m, "bytes_in")) + assert.Nil(t, mapValue(t, m, "bytes_out")) + assert.Nil(t, mapValue(t, m, "responsetime")) + assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) + assert.Equal(t, DuplicateQueryMsg.Error(), mapValue(t, m, "notes")) +} + +// Checks that PrepareNewMessage and Parse can manage two messages sharing one packet on the same stream +// It typically happens when a SOA is followed by AXFR +func TestParseTcp_errorDuplicateRequestsOnePacket(t *testing.T) { + var private protos.ProtocolData + dns := newDns(testing.Verbose()) + q := elasticATcp + offset := 4 + + concatRequest := append(q.request, q.request[:offset]...) + packet := newPacket(forward, concatRequest) + tcptuple := testTcpTuple() + + private = dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + + packet = newPacket(forward, q.request[offset:]) + private = dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + + m := expectResult(t, dns) + assertRequest(t, m, q) + assert.Equal(t, "tcp", mapValue(t, m, "transport")) + assert.Equal(t, len(q.request), mapValue(t, m, "bytes_in")) + assert.Nil(t, mapValue(t, m, "bytes_out")) + assert.Nil(t, mapValue(t, m, "responsetime")) + assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) + assert.Equal(t, DuplicateQueryMsg.Error(), mapValue(t, m, "notes")) +} + +// Verify that a split response packet is parsed and published +func TestParseTcp_splitResponse(t *testing.T) { + var private protos.ProtocolData + dns := newDns(testing.Verbose()) + tcpQuery := elasticATcp + q := tcpQuery.request + r0 := tcpQuery.response[:1] + r1 := tcpQuery.response[1:10] + r2 := tcpQuery.response[10:] + tcptuple := testTcpTuple() + + packet := newPacket(forward, q) + private = dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + + packet = newPacket(reverse, r0) + private = dns.Parse(packet, tcptuple, tcp.TcpDirectionReverse, private) + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + + packet = newPacket(reverse, r1) + dns.Parse(packet, tcptuple, tcp.TcpDirectionReverse, private) + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + + packet = newPacket(reverse, r2) + dns.Parse(packet, tcptuple, tcp.TcpDirectionReverse, private) + assert.Empty(t, dns.transactions.Size(), "There should be no transaction.") + + m := expectResult(t, dns) + assert.Equal(t, "tcp", mapValue(t, m, "transport")) + assert.Equal(t, len(tcpQuery.request), mapValue(t, m, "bytes_in")) + assert.Equal(t, len(tcpQuery.response), mapValue(t, m, "bytes_out")) + assert.NotNil(t, mapValue(t, m, "responsetime")) + assert.Equal(t, common.OK_STATUS, mapValue(t, m, "status")) + assert.Nil(t, mapValue(t, m, "notes")) + assertMapStrData(t, m, tcpQuery) +} + +func TestGap_requestDrop(t *testing.T) { + var private protos.ProtocolData + dns := newDns(testing.Verbose()) + q := sophosTxtTcp.request[:10] + packet := newPacket(forward, q) + tcptuple := testTcpTuple() + + private = dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + + private, drop := dns.GapInStream(tcptuple, tcp.TcpDirectionOriginal, 10, private) + + assert.Equal(t, true, drop) + + dns.ReceivedFin(tcptuple, tcp.TcpDirectionOriginal, private) + + client := dns.results.(publisher.ChanClient) + close(client.Channel) + mapStr := <-client.Channel + assert.Nil(t, mapStr, "No result should have been published.") +} + +// Verify that a gap during the response publish the request with Notes +func TestGap_errorResponse(t *testing.T) { + var private protos.ProtocolData + dns := newDns(testing.Verbose()) + q := sophosTxtTcp.request + r := sophosTxtTcp.response[:10] + tcptuple := testTcpTuple() + + packet := newPacket(forward, q) + private = dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + + packet = newPacket(reverse, r) + private = dns.Parse(packet, tcptuple, tcp.TcpDirectionReverse, private) + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + + private, drop := dns.GapInStream(tcptuple, tcp.TcpDirectionReverse, 10, private) + assert.Equal(t, true, drop) + + dns.ReceivedFin(tcptuple, tcp.TcpDirectionReverse, private) + + m := expectResult(t, dns) + assertRequest(t, m, sophosTxtTcp) + assert.Equal(t, IncompleteMsg.ResponseError(), mapValue(t, m, "notes")) + assert.Nil(t, mapValue(t, m, "answers")) +} + +// Verify that a gap/fin happening after a valid query create only one tansaction +func TestGapFin_validMessage(t *testing.T) { + var private protos.ProtocolData + dns := newDns(testing.Verbose()) + q := sophosTxtTcp.request + tcptuple := testTcpTuple() + + packet := newPacket(forward, q) + private = dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + + private, drop := dns.GapInStream(tcptuple, tcp.TcpDirectionOriginal, 10, private) + assert.Equal(t, false, drop) + + dns.ReceivedFin(tcptuple, tcp.TcpDirectionReverse, private) + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + + client := dns.results.(publisher.ChanClient) + close(client.Channel) + mapStr := <-client.Channel + assert.Nil(t, mapStr, "No result should have been published.") + assert.Empty(t, mapStr["notes"], "There should be no notes") +} + +// Verify that a Fin during the response publish the request with Notes +func TestFin_errorResponse(t *testing.T) { + var private protos.ProtocolData + dns := newDns(testing.Verbose()) + q := zoneAxfrTcp.request + r := zoneAxfrTcp.response[:10] + tcptuple := testTcpTuple() + + packet := newPacket(forward, q) + private = dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + + packet = newPacket(reverse, r) + private = dns.Parse(packet, tcptuple, tcp.TcpDirectionReverse, private) + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + + dns.ReceivedFin(tcptuple, tcp.TcpDirectionReverse, private) + + m := expectResult(t, dns) + assertRequest(t, m, zoneAxfrTcp) + assert.Equal(t, IncompleteMsg.ResponseError(), mapValue(t, m, "notes")) + assert.Nil(t, mapValue(t, m, "answers")) +} + +// parseTcpRequestResponse parses a request then a response packet and validates +// the published result. +func parseTcpRequestResponse(t testing.TB, dns *Dns, q DnsTestMessage) { + var private protos.ProtocolData + packet := newPacket(forward, q.request) + tcptuple := testTcpTuple() + private = dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + + packet = newPacket(reverse, q.response) + dns.Parse(packet, tcptuple, tcp.TcpDirectionReverse, private) + + assert.Empty(t, dns.transactions.Size(), "There should be no transactions.") + + m := expectResult(t, dns) + assert.Equal(t, "tcp", mapValue(t, m, "transport")) + assert.Equal(t, len(q.request), mapValue(t, m, "bytes_in")) + assert.Equal(t, len(q.response), mapValue(t, m, "bytes_out")) + assert.NotNil(t, mapValue(t, m, "responsetime")) + + if assert.ObjectsAreEqual("NOERROR", mapValue(t, m, "dns.response_code")) { + assert.Equal(t, common.OK_STATUS, mapValue(t, m, "status")) + } else { + assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) + } + + assert.Nil(t, mapValue(t, m, "notes")) + assertMapStrData(t, m, q) +} + +// Verify that the request/response pair are parsed and that a result +// is published. +func TestParseTcp_requestResponse(t *testing.T) { + parseTcpRequestResponse(t, newDns(testing.Verbose()), elasticATcp) +} + +// Verify all DNS TCP test messages are parsed correctly. +func TestParseTcp_allTestMessages(t *testing.T) { + dns := newDns(testing.Verbose()) + for _, q := range messagesTcp { + t.Logf("Testing with query for %s", q.q_name) + parseTcpRequestResponse(t, dns, q) + } +} + +// Benchmarks TCP parsing for the given test message. +func benchmarkTcp(b *testing.B, q DnsTestMessage) { + dns := newDns(false) + for i := 0; i < b.N; i++ { + var private protos.ProtocolData + packet := newPacket(forward, q.request) + tcptuple := testTcpTuple() + private = dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + + packet = newPacket(reverse, q.response) + dns.Parse(packet, tcptuple, tcp.TcpDirectionReverse, private) + + client := dns.results.(publisher.ChanClient) + <-client.Channel + } +} + +// Benchmark Tcp parsing against each test message. +func BenchmarkTcpElasticA(b *testing.B) { benchmarkTcp(b, elasticATcp) } +func BenchmarkTcpZoneIxfr(b *testing.B) { benchmarkTcp(b, zoneAxfrTcp) } +func BenchmarkTcpGithubPtr(b *testing.B) { benchmarkTcp(b, githubPtrTcp) } +func BenchmarkTcpSophosTxt(b *testing.B) { benchmarkTcp(b, sophosTxtTcp) } + +// Benchmark that runs with parallelism to help find concurrency related +// issues. To run with parallelism, the 'go test' cpu flag must be set +// greater than 1, otherwise it just runs concurrently but not in parallel. +func BenchmarkParallelTcpParse(b *testing.B) { + rand.Seed(22) + numMessages := len(messagesTcp) + dns := newDns(false) + client := dns.results.(publisher.ChanClient) + + // Drain the results channel while the test is running. + go func() { + totalMessages := 0 + for r := range client.Channel { + _ = r + totalMessages++ + } + fmt.Printf("Parsed %d messages.\n", totalMessages) + }() + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + // Each iteration parses one message, either a request or a response. + // The request and response could be parsed on different goroutines. + for pb.Next() { + q := messagesTcp[rand.Intn(numMessages)] + var packet *protos.Packet + var tcptuple *common.TcpTuple + var private protos.ProtocolData + + if rand.Intn(2) == 0 { + packet = newPacket(forward, q.request) + tcptuple = testTcpTuple() + } else { + packet = newPacket(reverse, q.response) + tcptuple = testTcpTuple() + } + dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) + } + }) + + defer close(client.Channel) +} diff --git a/packetbeat/protos/dns/dns_test.go b/packetbeat/protos/dns/dns_test.go index a1afe70fb43..07ceeeb3448 100644 --- a/packetbeat/protos/dns/dns_test.go +++ b/packetbeat/protos/dns/dns_test.go @@ -1,37 +1,20 @@ -// Unit tests and benchmarks for the dns package. -// -// The byte array test data was generated from pcap files using the gopacket -// test_creator.py script contained in the gopacket repository. The script was -// modified to drop the Ethernet, IP, and UDP headers from the byte arrays -// (skip the first 42 bytes for UDP packets and the first 54 bytes for TCP packets). -// -// TODO: -// * Add test validation for responsetime to make sure unit conversion -// is being done correctly. -// * Add validation of special fields provided in MX, SOA, NS queries. -// * Add test case to verify that Include_authorities and Include_additionals -// are working. -// * Add test case for Send_request and validate the stringified DNS message. -// * Add test case for Send_response and validate the stringified DNS message. +// Common variables, functions and tests for the dns package tests package dns import ( "fmt" - "math/rand" "net" "strings" "testing" "time" "github.com/elastic/beats/packetbeat/protos" - "github.com/elastic/beats/packetbeat/protos/tcp" "github.com/elastic/beats/libbeat/common" "github.com/elastic/beats/libbeat/logp" "github.com/elastic/beats/libbeat/publisher" "github.com/stretchr/testify/assert" - "github.com/tsg/gopacket/layers" ) // Test Constants @@ -59,302 +42,6 @@ type DnsTestMessage struct { response []byte } -// DNS messages for testing. When adding a new test message, add it to the -// messages array and create a new benchmark test for the message. -var ( - // An array of all test messages. - messages = []DnsTestMessage{ - elasticA, - zoneIxfr, - githubPtr, - sophosTxt, - } - messagesTcp = []DnsTestMessage{ - elasticATcp, - zoneAxfrTcp, - githubPtrTcp, - sophosTxtTcp, - } - - elasticA = DnsTestMessage{ - id: 8529, - opcode: "QUERY", - flags: []string{"rd", "ra"}, - rcode: "NOERROR", - q_class: "IN", - q_type: "A", - q_name: "elastic.co", - answers: []string{"54.148.130.30", "54.69.104.66"}, - request: []byte{ - 0x21, 0x51, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x65, 0x6c, 0x61, - 0x73, 0x74, 0x69, 0x63, 0x02, 0x63, 0x6f, 0x00, 0x00, 0x01, 0x00, 0x01, - }, - response: []byte{ - 0x21, 0x51, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x07, 0x65, 0x6c, 0x61, - 0x73, 0x74, 0x69, 0x63, 0x02, 0x63, 0x6f, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x01, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x39, 0x00, 0x04, 0x36, 0x94, 0x82, 0x1e, 0xc0, 0x0c, 0x00, 0x01, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x39, 0x00, 0x04, 0x36, 0x45, 0x68, 0x42, - }, - } - - elasticATcp = DnsTestMessage{ - id: 11674, - opcode: "QUERY", - flags: []string{"rd", "ra"}, - rcode: "NOERROR", - q_class: "IN", - q_type: "A", - q_name: "elastic.co", - answers: []string{"54.201.204.244", "54.200.185.88"}, - authorities: []string{"NS-835.AWSDNS-40.NET", "NS-1183.AWSDNS-19.ORG", "NS-2007.AWSDNS-58.CO.UK", "NS-66.AWSDNS-08.COM"}, - request: []byte{ - 0x00, 0x1c, 0x2d, 0x9a, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x65, - 0x6c, 0x61, 0x73, 0x74, 0x69, 0x63, 0x02, 0x63, 0x6f, 0x00, 0x00, 0x01, 0x00, 0x01, - }, - response: []byte{ - 0x00, 0xc7, 0x2d, 0x9a, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x07, 0x65, - 0x6c, 0x61, 0x73, 0x74, 0x69, 0x63, 0x02, 0x63, 0x6f, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, - 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x04, 0x36, 0xc8, 0xb9, 0x58, 0xc0, 0x0c, - 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x04, 0x36, 0xc9, 0xcc, 0xf4, 0xc0, 0x0c, - 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x16, 0x82, 0x00, 0x16, 0x06, 0x4e, 0x53, 0x2d, 0x38, 0x33, - 0x35, 0x09, 0x41, 0x57, 0x53, 0x44, 0x4e, 0x53, 0x2d, 0x34, 0x30, 0x03, 0x4e, 0x45, 0x54, 0x00, - 0xc0, 0x0c, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x16, 0x82, 0x00, 0x17, 0x07, 0x4e, 0x53, 0x2d, - 0x31, 0x31, 0x38, 0x33, 0x09, 0x41, 0x57, 0x53, 0x44, 0x4e, 0x53, 0x2d, 0x31, 0x39, 0x03, 0x4f, - 0x52, 0x47, 0x00, 0xc0, 0x0c, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x16, 0x82, 0x00, 0x19, 0x07, - 0x4e, 0x53, 0x2d, 0x32, 0x30, 0x30, 0x37, 0x09, 0x41, 0x57, 0x53, 0x44, 0x4e, 0x53, 0x2d, 0x35, - 0x38, 0x02, 0x43, 0x4f, 0x02, 0x55, 0x4b, 0x00, 0xc0, 0x0c, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, - 0x16, 0x82, 0x00, 0x15, 0x05, 0x4e, 0x53, 0x2d, 0x36, 0x36, 0x09, 0x41, 0x57, 0x53, 0x44, 0x4e, - 0x53, 0x2d, 0x30, 0x38, 0x03, 0x43, 0x4f, 0x4d, 0x00, - }, - } - - zoneIxfr = DnsTestMessage{ - id: 16384, - opcode: "QUERY", - flags: []string{"ra"}, - rcode: "NOERROR", - q_class: "IN", - q_type: "IXFR", - q_name: "etas.com", - answers: []string{"training2003p", "training2003p", "training2003p", - "training2003p", "1.1.1.100"}, - request: []byte{ - 0x40, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x04, 0x65, 0x74, 0x61, - 0x73, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0xfb, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x06, 0x00, 0x01, - 0x00, 0x00, 0x0e, 0x10, 0x00, 0x2f, 0x0d, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x32, - 0x30, 0x30, 0x33, 0x70, 0x00, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, - 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x02, 0x58, 0x00, 0x01, 0x51, - 0x80, 0x00, 0x00, 0x0e, 0x10, 0x4d, 0x53, - }, - response: []byte{ - 0x40, 0x00, 0x80, 0x80, 0x00, 0x01, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x04, 0x65, 0x74, 0x61, - 0x73, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0xfb, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x06, 0x00, 0x01, - 0x00, 0x00, 0x0e, 0x10, 0x00, 0x2f, 0x0d, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x32, - 0x30, 0x30, 0x33, 0x70, 0x00, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, - 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x02, 0x58, 0x00, 0x01, 0x51, - 0x80, 0x00, 0x00, 0x0e, 0x10, 0xc0, 0x0c, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x0e, 0x10, 0x00, - 0x18, 0xc0, 0x26, 0xc0, 0x35, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x02, - 0x58, 0x00, 0x01, 0x51, 0x80, 0x00, 0x00, 0x0e, 0x10, 0xc0, 0x0c, 0x00, 0x06, 0x00, 0x01, 0x00, - 0x00, 0x0e, 0x10, 0x00, 0x18, 0xc0, 0x26, 0xc0, 0x35, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, - 0x3c, 0x00, 0x00, 0x02, 0x58, 0x00, 0x01, 0x51, 0x80, 0x00, 0x00, 0x0e, 0x10, 0x05, 0x69, 0x6e, - 0x64, 0x65, 0x78, 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x0e, 0x10, 0x00, 0x04, 0x01, - 0x01, 0x01, 0x64, 0xc0, 0x0c, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x0e, 0x10, 0x00, 0x18, 0xc0, - 0x26, 0xc0, 0x35, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x02, 0x58, 0x00, - 0x01, 0x51, 0x80, 0x00, 0x00, 0x0e, 0x10, - }, - } - - zoneAxfrTcp = DnsTestMessage{ - id: 0, - opcode: "QUERY", - rcode: "NOERROR", - q_class: "IN", - q_type: "AXFR", - q_name: "etas.com", - answers: []string{"training2003p", "training2003p", "1.1.1.1", "training2003p"}, - request: []byte{ - 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x65, - 0x74, 0x61, 0x73, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0xfc, 0x00, 0x01, 0x4d, 0x53, - }, - response: []byte{ - 0x00, 0xc3, 0x00, 0x00, 0x80, 0x80, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x04, 0x65, - 0x74, 0x61, 0x73, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0xfc, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x06, - 0x00, 0x01, 0x00, 0x00, 0x0e, 0x10, 0x00, 0x2f, 0x0d, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, - 0x67, 0x32, 0x30, 0x30, 0x33, 0x70, 0x00, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x6d, 0x61, 0x73, 0x74, - 0x65, 0x72, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x02, 0x58, 0x00, - 0x01, 0x51, 0x80, 0x00, 0x00, 0x0e, 0x10, 0xc0, 0x0c, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x0e, - 0x10, 0x00, 0x0f, 0x0d, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x32, 0x30, 0x30, 0x33, - 0x70, 0x00, 0x07, 0x77, 0x65, 0x6c, 0x63, 0x6f, 0x6d, 0x65, 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, - 0x00, 0x00, 0x0e, 0x10, 0x00, 0x04, 0x01, 0x01, 0x01, 0x01, 0xc0, 0x0c, 0x00, 0x06, 0x00, 0x01, - 0x00, 0x00, 0x0e, 0x10, 0x00, 0x2f, 0x0d, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x32, - 0x30, 0x30, 0x33, 0x70, 0x00, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, - 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x02, 0x58, 0x00, 0x01, 0x51, - 0x80, 0x00, 0x00, 0x0e, 0x10, - }, - } - - githubPtr = DnsTestMessage{ - id: 344, - opcode: "QUERY", - flags: []string{"rd", "ra"}, - rcode: "NOERROR", - q_class: "IN", - q_type: "PTR", - q_name: "131.252.30.192.in-addr.arpa", - answers: []string{"github.com"}, - authorities: []string{"a.root-servers.net", "b.root-servers.net", "c.root-servers.net", - "d.root-servers.net", "e.root-servers.net", "f.root-servers.net", "g.root-servers.net", - "h.root-servers.net", "i.root-servers.net", "j.root-servers.net", "k.root-servers.net", - "l.root-servers.net", "m.root-servers.net"}, - request: []byte{ - 0x01, 0x58, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x31, 0x33, 0x31, - 0x03, 0x32, 0x35, 0x32, 0x02, 0x33, 0x30, 0x03, 0x31, 0x39, 0x32, 0x07, 0x69, 0x6e, 0x2d, 0x61, - 0x64, 0x64, 0x72, 0x04, 0x61, 0x72, 0x70, 0x61, 0x00, 0x00, 0x0c, 0x00, 0x01, - }, - response: []byte{ - 0x01, 0x58, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x0d, 0x00, 0x00, 0x03, 0x31, 0x33, 0x31, - 0x03, 0x32, 0x35, 0x32, 0x02, 0x33, 0x30, 0x03, 0x31, 0x39, 0x32, 0x07, 0x69, 0x6e, 0x2d, 0x61, - 0x64, 0x64, 0x72, 0x04, 0x61, 0x72, 0x70, 0x61, 0x00, 0x00, 0x0c, 0x00, 0x01, 0xc0, 0x0c, 0x00, - 0x0c, 0x00, 0x01, 0x00, 0x00, 0x09, 0xe2, 0x00, 0x0c, 0x06, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x14, - 0x01, 0x6c, 0x0c, 0x72, 0x6f, 0x6f, 0x74, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x03, - 0x6e, 0x65, 0x74, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, - 0x65, 0xc0, 0x52, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x63, - 0xc0, 0x52, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x62, 0xc0, - 0x52, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x61, 0xc0, 0x52, - 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x68, 0xc0, 0x52, 0x00, - 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x66, 0xc0, 0x52, 0x00, 0x00, - 0x02, 0x00, 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x69, 0xc0, 0x52, 0x00, 0x00, 0x02, - 0x00, 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x67, 0xc0, 0x52, 0x00, 0x00, 0x02, 0x00, - 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x6d, 0xc0, 0x52, 0x00, 0x00, 0x02, 0x00, 0x01, - 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x64, 0xc0, 0x52, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, - 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x6a, 0xc0, 0x52, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, - 0x07, 0xb8, 0x00, 0x04, 0x01, 0x6b, 0xc0, 0x52, - }, - } - - githubPtrTcp = DnsTestMessage{ - id: 6766, - opcode: "QUERY", - flags: []string{"rd", "ra"}, - rcode: "NOERROR", - q_class: "IN", - q_type: "PTR", - q_name: "131.252.30.192.in-addr.arpa", - answers: []string{"github.com"}, - authorities: []string{"ns1.p16.dynect.net", "ns3.p16.dynect.net", "ns4.p16.dynect.net", "ns2.p16.dynect.net"}, - request: []byte{ - 0x00, 0x2d, 0x1a, 0x6e, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x31, - 0x33, 0x31, 0x03, 0x32, 0x35, 0x32, 0x02, 0x33, 0x30, 0x03, 0x31, 0x39, 0x32, 0x07, 0x69, 0x6e, - 0x2d, 0x61, 0x64, 0x64, 0x72, 0x04, 0x61, 0x72, 0x70, 0x61, 0x00, 0x00, 0x0c, 0x00, 0x01, - }, - response: []byte{ - 0x00, 0x9b, 0x1a, 0x6e, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x04, 0x00, 0x00, 0x03, 0x31, - 0x33, 0x31, 0x03, 0x32, 0x35, 0x32, 0x02, 0x33, 0x30, 0x03, 0x31, 0x39, 0x32, 0x07, 0x69, 0x6e, - 0x2d, 0x61, 0x64, 0x64, 0x72, 0x04, 0x61, 0x72, 0x70, 0x61, 0x00, 0x00, 0x0c, 0x00, 0x01, 0xc0, - 0x0c, 0x00, 0x0c, 0x00, 0x01, 0x00, 0x00, 0x0e, 0x07, 0x00, 0x0c, 0x06, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0xc0, 0x10, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x51, - 0x77, 0x00, 0x14, 0x03, 0x6e, 0x73, 0x31, 0x03, 0x70, 0x31, 0x36, 0x06, 0x64, 0x79, 0x6e, 0x65, - 0x63, 0x74, 0x03, 0x6e, 0x65, 0x74, 0x00, 0xc0, 0x10, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x51, - 0x77, 0x00, 0x06, 0x03, 0x6e, 0x73, 0x33, 0xc0, 0x55, 0xc0, 0x10, 0x00, 0x02, 0x00, 0x01, 0x00, - 0x01, 0x51, 0x77, 0x00, 0x06, 0x03, 0x6e, 0x73, 0x34, 0xc0, 0x55, 0xc0, 0x10, 0x00, 0x02, 0x00, - 0x01, 0x00, 0x01, 0x51, 0x77, 0x00, 0x06, 0x03, 0x6e, 0x73, 0x32, 0xc0, 0x55, - }, - } - - sophosTxt = DnsTestMessage{ - id: 8238, - opcode: "QUERY", - flags: []string{"rd", "ra"}, - rcode: "NXDOMAIN", - q_class: "IN", - q_type: "TXT", - q_name: "3.1o19ss00s2s17s4qp375sp49r830n2n4n923s8839052s7p7768s53365226pp3.659p1r741os37393" + - "648s2348o762q1066q53rq5p4614r1q4781qpr16n809qp4.879o3o734q9sns005o3pp76q83.2q65qns3spns" + - "1081s5rn5sr74opqrqnpq6rn3ro5.i.00.mac.sophosxl.net", - request: []byte{ - 0x20, 0x2e, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x33, 0x3f, 0x31, - 0x6f, 0x31, 0x39, 0x73, 0x73, 0x30, 0x30, 0x73, 0x32, 0x73, 0x31, 0x37, 0x73, 0x34, 0x71, 0x70, - 0x33, 0x37, 0x35, 0x73, 0x70, 0x34, 0x39, 0x72, 0x38, 0x33, 0x30, 0x6e, 0x32, 0x6e, 0x34, 0x6e, - 0x39, 0x32, 0x33, 0x73, 0x38, 0x38, 0x33, 0x39, 0x30, 0x35, 0x32, 0x73, 0x37, 0x70, 0x37, 0x37, - 0x36, 0x38, 0x73, 0x35, 0x33, 0x33, 0x36, 0x35, 0x32, 0x32, 0x36, 0x70, 0x70, 0x33, 0x3f, 0x36, - 0x35, 0x39, 0x70, 0x31, 0x72, 0x37, 0x34, 0x31, 0x6f, 0x73, 0x33, 0x37, 0x33, 0x39, 0x33, 0x36, - 0x34, 0x38, 0x73, 0x32, 0x33, 0x34, 0x38, 0x6f, 0x37, 0x36, 0x32, 0x71, 0x31, 0x30, 0x36, 0x36, - 0x71, 0x35, 0x33, 0x72, 0x71, 0x35, 0x70, 0x34, 0x36, 0x31, 0x34, 0x72, 0x31, 0x71, 0x34, 0x37, - 0x38, 0x31, 0x71, 0x70, 0x72, 0x31, 0x36, 0x6e, 0x38, 0x30, 0x39, 0x71, 0x70, 0x34, 0x1a, 0x38, - 0x37, 0x39, 0x6f, 0x33, 0x6f, 0x37, 0x33, 0x34, 0x71, 0x39, 0x73, 0x6e, 0x73, 0x30, 0x30, 0x35, - 0x6f, 0x33, 0x70, 0x70, 0x37, 0x36, 0x71, 0x38, 0x33, 0x28, 0x32, 0x71, 0x36, 0x35, 0x71, 0x6e, - 0x73, 0x33, 0x73, 0x70, 0x6e, 0x73, 0x31, 0x30, 0x38, 0x31, 0x73, 0x35, 0x72, 0x6e, 0x35, 0x73, - 0x72, 0x37, 0x34, 0x6f, 0x70, 0x71, 0x72, 0x71, 0x6e, 0x70, 0x71, 0x36, 0x72, 0x6e, 0x33, 0x72, - 0x6f, 0x35, 0x01, 0x69, 0x02, 0x30, 0x30, 0x03, 0x6d, 0x61, 0x63, 0x08, 0x73, 0x6f, 0x70, 0x68, - 0x6f, 0x73, 0x78, 0x6c, 0x03, 0x6e, 0x65, 0x74, 0x00, 0x00, 0x10, 0x00, 0x01, - }, - response: []byte{ - 0x20, 0x2e, 0x81, 0x83, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x33, 0x3f, 0x31, - 0x6f, 0x31, 0x39, 0x73, 0x73, 0x30, 0x30, 0x73, 0x32, 0x73, 0x31, 0x37, 0x73, 0x34, 0x71, 0x70, - 0x33, 0x37, 0x35, 0x73, 0x70, 0x34, 0x39, 0x72, 0x38, 0x33, 0x30, 0x6e, 0x32, 0x6e, 0x34, 0x6e, - 0x39, 0x32, 0x33, 0x73, 0x38, 0x38, 0x33, 0x39, 0x30, 0x35, 0x32, 0x73, 0x37, 0x70, 0x37, 0x37, - 0x36, 0x38, 0x73, 0x35, 0x33, 0x33, 0x36, 0x35, 0x32, 0x32, 0x36, 0x70, 0x70, 0x33, 0x3f, 0x36, - 0x35, 0x39, 0x70, 0x31, 0x72, 0x37, 0x34, 0x31, 0x6f, 0x73, 0x33, 0x37, 0x33, 0x39, 0x33, 0x36, - 0x34, 0x38, 0x73, 0x32, 0x33, 0x34, 0x38, 0x6f, 0x37, 0x36, 0x32, 0x71, 0x31, 0x30, 0x36, 0x36, - 0x71, 0x35, 0x33, 0x72, 0x71, 0x35, 0x70, 0x34, 0x36, 0x31, 0x34, 0x72, 0x31, 0x71, 0x34, 0x37, - 0x38, 0x31, 0x71, 0x70, 0x72, 0x31, 0x36, 0x6e, 0x38, 0x30, 0x39, 0x71, 0x70, 0x34, 0x1a, 0x38, - 0x37, 0x39, 0x6f, 0x33, 0x6f, 0x37, 0x33, 0x34, 0x71, 0x39, 0x73, 0x6e, 0x73, 0x30, 0x30, 0x35, - 0x6f, 0x33, 0x70, 0x70, 0x37, 0x36, 0x71, 0x38, 0x33, 0x28, 0x32, 0x71, 0x36, 0x35, 0x71, 0x6e, - 0x73, 0x33, 0x73, 0x70, 0x6e, 0x73, 0x31, 0x30, 0x38, 0x31, 0x73, 0x35, 0x72, 0x6e, 0x35, 0x73, - 0x72, 0x37, 0x34, 0x6f, 0x70, 0x71, 0x72, 0x71, 0x6e, 0x70, 0x71, 0x36, 0x72, 0x6e, 0x33, 0x72, - 0x6f, 0x35, 0x01, 0x69, 0x02, 0x30, 0x30, 0x03, 0x6d, 0x61, 0x63, 0x08, 0x73, 0x6f, 0x70, 0x68, - 0x6f, 0x73, 0x78, 0x6c, 0x03, 0x6e, 0x65, 0x74, 0x00, 0x00, 0x10, 0x00, 0x01, - }, - } - - sophosTxtTcp = DnsTestMessage{ - id: 35009, - opcode: "QUERY", - flags: []string{"rd", "ra"}, - rcode: "NXDOMAIN", - q_class: "IN", - q_type: "TXT", - q_name: "3.1o19ss00s2s17s4qp375sp49r830n2n4n923s8839052s7p7768s53365226pp3.659p1r741os37393" + - "648s2348o762q1066q53rq5p4614r1q4781qpr16n809qp4.879o3o734q9sns005o3pp76q83.2q65qns3spns" + - "1081s5rn5sr74opqrqnpq6rn3ro5.i.00.mac.sophosxl.net", - request: []byte{ - 0x00, 0xed, 0x88, 0xc1, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x33, - 0x3f, 0x31, 0x6f, 0x31, 0x39, 0x73, 0x73, 0x30, 0x30, 0x73, 0x32, 0x73, 0x31, 0x37, 0x73, 0x34, - 0x71, 0x70, 0x33, 0x37, 0x35, 0x73, 0x70, 0x34, 0x39, 0x72, 0x38, 0x33, 0x30, 0x6e, 0x32, 0x6e, - 0x34, 0x6e, 0x39, 0x32, 0x33, 0x73, 0x38, 0x38, 0x33, 0x39, 0x30, 0x35, 0x32, 0x73, 0x37, 0x70, - 0x37, 0x37, 0x36, 0x38, 0x73, 0x35, 0x33, 0x33, 0x36, 0x35, 0x32, 0x32, 0x36, 0x70, 0x70, 0x33, - 0x3f, 0x36, 0x35, 0x39, 0x70, 0x31, 0x72, 0x37, 0x34, 0x31, 0x6f, 0x73, 0x33, 0x37, 0x33, 0x39, - 0x33, 0x36, 0x34, 0x38, 0x73, 0x32, 0x33, 0x34, 0x38, 0x6f, 0x37, 0x36, 0x32, 0x71, 0x31, 0x30, - 0x36, 0x36, 0x71, 0x35, 0x33, 0x72, 0x71, 0x35, 0x70, 0x34, 0x36, 0x31, 0x34, 0x72, 0x31, 0x71, - 0x34, 0x37, 0x38, 0x31, 0x71, 0x70, 0x72, 0x31, 0x36, 0x6e, 0x38, 0x30, 0x39, 0x71, 0x70, 0x34, - 0x1a, 0x38, 0x37, 0x39, 0x6f, 0x33, 0x6f, 0x37, 0x33, 0x34, 0x71, 0x39, 0x73, 0x6e, 0x73, 0x30, - 0x30, 0x35, 0x6f, 0x33, 0x70, 0x70, 0x37, 0x36, 0x71, 0x38, 0x33, 0x28, 0x32, 0x71, 0x36, 0x35, - 0x71, 0x6e, 0x73, 0x33, 0x73, 0x70, 0x6e, 0x73, 0x31, 0x30, 0x38, 0x31, 0x73, 0x35, 0x72, 0x6e, - 0x35, 0x73, 0x72, 0x37, 0x34, 0x6f, 0x70, 0x71, 0x72, 0x71, 0x6e, 0x70, 0x71, 0x36, 0x72, 0x6e, - 0x33, 0x72, 0x6f, 0x35, 0x01, 0x69, 0x02, 0x30, 0x30, 0x03, 0x6d, 0x61, 0x63, 0x08, 0x73, 0x6f, - 0x70, 0x68, 0x6f, 0x73, 0x78, 0x6c, 0x03, 0x6e, 0x65, 0x74, 0x00, 0x00, 0x10, 0x00, 0x01, - }, - response: []byte{ - 0x00, 0xed, 0x88, 0xc1, 0x81, 0x83, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x33, - 0x3f, 0x31, 0x6f, 0x31, 0x39, 0x73, 0x73, 0x30, 0x30, 0x73, 0x32, 0x73, 0x31, 0x37, 0x73, 0x34, - 0x71, 0x70, 0x33, 0x37, 0x35, 0x73, 0x70, 0x34, 0x39, 0x72, 0x38, 0x33, 0x30, 0x6e, 0x32, 0x6e, - 0x34, 0x6e, 0x39, 0x32, 0x33, 0x73, 0x38, 0x38, 0x33, 0x39, 0x30, 0x35, 0x32, 0x73, 0x37, 0x70, - 0x37, 0x37, 0x36, 0x38, 0x73, 0x35, 0x33, 0x33, 0x36, 0x35, 0x32, 0x32, 0x36, 0x70, 0x70, 0x33, - 0x3f, 0x36, 0x35, 0x39, 0x70, 0x31, 0x72, 0x37, 0x34, 0x31, 0x6f, 0x73, 0x33, 0x37, 0x33, 0x39, - 0x33, 0x36, 0x34, 0x38, 0x73, 0x32, 0x33, 0x34, 0x38, 0x6f, 0x37, 0x36, 0x32, 0x71, 0x31, 0x30, - 0x36, 0x36, 0x71, 0x35, 0x33, 0x72, 0x71, 0x35, 0x70, 0x34, 0x36, 0x31, 0x34, 0x72, 0x31, 0x71, - 0x34, 0x37, 0x38, 0x31, 0x71, 0x70, 0x72, 0x31, 0x36, 0x6e, 0x38, 0x30, 0x39, 0x71, 0x70, 0x34, - 0x1a, 0x38, 0x37, 0x39, 0x6f, 0x33, 0x6f, 0x37, 0x33, 0x34, 0x71, 0x39, 0x73, 0x6e, 0x73, 0x30, - 0x30, 0x35, 0x6f, 0x33, 0x70, 0x70, 0x37, 0x36, 0x71, 0x38, 0x33, 0x28, 0x32, 0x71, 0x36, 0x35, - 0x71, 0x6e, 0x73, 0x33, 0x73, 0x70, 0x6e, 0x73, 0x31, 0x30, 0x38, 0x31, 0x73, 0x35, 0x72, 0x6e, - 0x35, 0x73, 0x72, 0x37, 0x34, 0x6f, 0x70, 0x71, 0x72, 0x71, 0x6e, 0x70, 0x71, 0x36, 0x72, 0x6e, - 0x33, 0x72, 0x6f, 0x35, 0x01, 0x69, 0x02, 0x30, 0x30, 0x03, 0x6d, 0x61, 0x63, 0x08, 0x73, 0x6f, - 0x70, 0x68, 0x6f, 0x73, 0x78, 0x6c, 0x03, 0x6e, 0x65, 0x74, 0x00, 0x00, 0x10, 0x00, 0x01, - }, - } -) - // Request and response addresses. var ( forward = common.NewIpPortTuple(4, @@ -365,10 +52,6 @@ var ( net.ParseIP(ServerIp), ServerPort) ) -// Verify that the interfaces for UDP and TCP have been satisfied. -var _ protos.UdpProtocolPlugin = &Dns{} -var _ protos.TcpProtocolPlugin = &Dns{} - func newDns(verbose bool) *Dns { if verbose { logp.LogInit(logp.LOG_DEBUG, "", false, true, []string{"dns"}) @@ -398,232 +81,6 @@ func newPacket(t common.IpPortTuple, payload []byte) *protos.Packet { } } -// Verify that nameToString encodes non-printable characters. -func Test_nameToString_encodesNonPrintable(t *testing.T) { - name := "\n \r \t \" \\ \u2318.dnstunnel.com" - escapedName := "\\n \\r \\t \\\" \\\\ \\226\\140\\152.dnstunnel.com" - assert.Equal(t, escapedName, nameToString([]byte(name))) -} - -// Verify that an empty packet is safely handled (no panics). -func TestParseUdp_emptyPacket(t *testing.T) { - dns := newDns(testing.Verbose()) - packet := newPacket(forward, []byte{}) - dns.ParseUdp(packet) - assert.Empty(t, dns.transactions.Size(), "There should be no transactions.") - client := dns.results.(publisher.ChanClient) - close(client.Channel) - assert.Nil(t, <-client.Channel, "No result should have been published.") -} - -// Verify that a malformed packet is safely handled (no panics). -func TestParseUdp_malformedPacket(t *testing.T) { - dns := newDns(testing.Verbose()) - garbage := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13} - packet := newPacket(forward, garbage) - dns.ParseUdp(packet) - assert.Empty(t, dns.transactions.Size(), "There should be no transactions.") - - // As a future addition, a malformed message should publish a result. -} - -// Verify that the lone request packet is parsed. -func TestParseUdp_requestPacket(t *testing.T) { - dns := newDns(testing.Verbose()) - packet := newPacket(forward, elasticA.request) - dns.ParseUdp(packet) - assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") - client := dns.results.(publisher.ChanClient) - close(client.Channel) - assert.Nil(t, <-client.Channel, "No result should have been published.") -} - -// Verify that the lone response packet is parsed and that an error -// result is published. -func TestParseUdp_responseOnly(t *testing.T) { - dns := newDns(testing.Verbose()) - q := elasticA - packet := newPacket(reverse, q.response) - dns.ParseUdp(packet) - - m := expectResult(t, dns) - assert.Equal(t, "udp", mapValue(t, m, "transport")) - assert.Nil(t, mapValue(t, m, "bytes_in")) - assert.Equal(t, len(q.response), mapValue(t, m, "bytes_out")) - assert.Nil(t, mapValue(t, m, "responsetime")) - assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) - assert.Equal(t, OrphanedResponseMsg, mapValue(t, m, "notes")) - assertMapStrData(t, m, q) -} - -// Verify that the first request is published without a response and that -// the status is error. This second packet will remain in the transaction -// map awaiting a response. -func TestParseUdp_duplicateRequests(t *testing.T) { - dns := newDns(testing.Verbose()) - q := elasticA - packet := newPacket(forward, q.request) - dns.ParseUdp(packet) - assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") - packet = newPacket(forward, q.request) - dns.ParseUdp(packet) - assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") - - m := expectResult(t, dns) - assert.Equal(t, "udp", mapValue(t, m, "transport")) - assert.Equal(t, len(q.request), mapValue(t, m, "bytes_in")) - assert.Nil(t, mapValue(t, m, "bytes_out")) - assert.Nil(t, mapValue(t, m, "responsetime")) - assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) - assert.Equal(t, DuplicateQueryMsg, mapValue(t, m, "notes")) -} - -// Verify that the request/response pair are parsed and that a result -// is published. -func TestParseUdp_requestResponse(t *testing.T) { - parseUdpRequestResponse(t, newDns(testing.Verbose()), elasticA) -} - -// Verify all DNS test messages are parsed correctly. -func TestParseUdp_allTestMessages(t *testing.T) { - dns := newDns(testing.Verbose()) - for _, q := range messages { - t.Logf("Testing with query for %s", q.q_name) - parseUdpRequestResponse(t, dns, q) - } -} - -// Verify that expireTransaction publishes an event with an error status -// and note. -func TestExpireTransaction(t *testing.T) { - dns := newDns(testing.Verbose()) - - trans := newTransaction(time.Now(), DnsTuple{}, common.CmdlineTuple{}) - trans.Request = &DnsMessage{ - Data: &layers.DNS{ - Questions: []layers.DNSQuestion{{}}, - }, - } - dns.expireTransaction(trans) - - m := expectResult(t, dns) - assert.Nil(t, mapValue(t, m, "bytes_out")) - assert.Nil(t, mapValue(t, m, "responsetime")) - assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) - assert.Equal(t, NoResponse, mapValue(t, m, "notes")) -} - -// Verify that an empty DNS request packet can be published. -func TestPublishTransaction_emptyDnsRequest(t *testing.T) { - dns := newDns(testing.Verbose()) - - trans := newTransaction(time.Now(), DnsTuple{}, common.CmdlineTuple{}) - trans.Request = &DnsMessage{ - Data: &layers.DNS{}, - } - dns.publishTransaction(trans) - - m := expectResult(t, dns) - assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) -} - -// Verify that an empty DNS response packet can be published. -func TestPublishTransaction_emptyDnsResponse(t *testing.T) { - dns := newDns(testing.Verbose()) - - trans := newTransaction(time.Now(), DnsTuple{}, common.CmdlineTuple{}) - trans.Response = &DnsMessage{ - Data: &layers.DNS{}, - } - dns.publishTransaction(trans) - - m := expectResult(t, dns) - assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) -} - -// Benchmarks UDP parsing for the given test message. -func benchmarkUdp(b *testing.B, q DnsTestMessage) { - dns := newDns(false) - for i := 0; i < b.N; i++ { - packet := newPacket(forward, q.request) - dns.ParseUdp(packet) - packet = newPacket(reverse, q.response) - dns.ParseUdp(packet) - - client := dns.results.(publisher.ChanClient) - <-client.Channel - } -} - -// Benchmark UDP parsing against each test message. -func BenchmarkUdpElasticA(b *testing.B) { benchmarkUdp(b, elasticA) } -func BenchmarkUdpZoneIxfr(b *testing.B) { benchmarkUdp(b, zoneIxfr) } -func BenchmarkUdpGithubPtr(b *testing.B) { benchmarkUdp(b, githubPtr) } -func BenchmarkUdpSophosTxt(b *testing.B) { benchmarkUdp(b, sophosTxt) } - -// Benchmark that runs with parallelism to help find concurrency related -// issues. To run with parallelism, the 'go test' cpu flag must be set -// greater than 1, otherwise it just runs concurrently but not in parallel. -func BenchmarkParallelUdpParse(b *testing.B) { - rand.Seed(22) - numMessages := len(messages) - dns := newDns(false) - client := dns.results.(publisher.ChanClient) - - // Drain the results channal while the test is running. - go func() { - totalMessages := 0 - for r := range client.Channel { - _ = r - totalMessages++ - } - fmt.Printf("Parsed %d messages.\n", totalMessages) - }() - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - // Each iteration parses one message, either a request or a response. - // The request and response could be parsed on different goroutines. - for pb.Next() { - q := messages[rand.Intn(numMessages)] - var packet *protos.Packet - if rand.Intn(2) == 0 { - packet = newPacket(forward, q.request) - } else { - packet = newPacket(reverse, q.response) - } - dns.ParseUdp(packet) - } - }) - - defer close(client.Channel) -} - -// parseUdpRequestResponse parses a request then a response packet and validates -// the published result. -func parseUdpRequestResponse(t testing.TB, dns *Dns, q DnsTestMessage) { - packet := newPacket(forward, q.request) - dns.ParseUdp(packet) - packet = newPacket(reverse, q.response) - dns.ParseUdp(packet) - assert.Empty(t, dns.transactions.Size(), "There should be no transactions.") - - m := expectResult(t, dns) - assert.Equal(t, "udp", mapValue(t, m, "transport")) - assert.Equal(t, len(q.request), mapValue(t, m, "bytes_in")) - assert.Equal(t, len(q.response), mapValue(t, m, "bytes_out")) - assert.NotNil(t, mapValue(t, m, "responsetime")) - - if assert.ObjectsAreEqual("NOERROR", mapValue(t, m, "dns.response_code")) { - assert.Equal(t, common.OK_STATUS, mapValue(t, m, "status")) - } else { - assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) - } - - assert.Nil(t, mapValue(t, m, "notes")) - assertMapStrData(t, m, q) -} - // expectResult returns one MapStr result from the Dns results channel. If // no result is available then the test fails. func expectResult(t testing.TB, dns *Dns) common.MapStr { @@ -696,22 +153,12 @@ func mapValueHelper(t testing.TB, m common.MapStr, keys []string) interface{} { // dns.additionals_count // dns.additionals func assertMapStrData(t testing.TB, m common.MapStr, q DnsTestMessage) { - assert.Equal(t, "dns", mapValue(t, m, "type")) - assertAddress(t, forward, mapValue(t, m, "src")) - assertAddress(t, reverse, mapValue(t, m, "dst")) - assert.Equal(t, fmt.Sprintf("class %s, type %s, %s", q.q_class, q.q_type, q.q_name), - mapValue(t, m, "query")) - assert.Equal(t, q.q_name, mapValue(t, m, "resource")) - assert.Equal(t, q.opcode, mapValue(t, m, "method")) - assert.Equal(t, q.id, mapValue(t, m, "dns.id")) - assert.Equal(t, q.opcode, mapValue(t, m, "dns.op_code")) + assertRequest(t, m, q) + + // Answers assertFlags(t, m, q.flags) assert.Equal(t, q.rcode, mapValue(t, m, "dns.response_code")) - assert.Equal(t, q.q_class, mapValue(t, m, "dns.question.class")) - assert.Equal(t, q.q_type, mapValue(t, m, "dns.question.type")) - assert.Equal(t, q.q_name, mapValue(t, m, "dns.question.name")) - // Answers assert.Equal(t, len(q.answers), mapValue(t, m, "dns.answers_count"), "Expected dns.answers_count to be %d", len(q.answers)) if len(q.answers) > 0 { @@ -751,6 +198,21 @@ func assertMapStrData(t testing.TB, m common.MapStr, q DnsTestMessage) { } } +func assertRequest(t testing.TB, m common.MapStr, q DnsTestMessage) { + assert.Equal(t, "dns", mapValue(t, m, "type")) + assertAddress(t, forward, mapValue(t, m, "src")) + assertAddress(t, reverse, mapValue(t, m, "dst")) + assert.Equal(t, fmt.Sprintf("class %s, type %s, %s", q.q_class, q.q_type, q.q_name), + mapValue(t, m, "query")) + assert.Equal(t, q.q_name, mapValue(t, m, "resource")) + assert.Equal(t, q.opcode, mapValue(t, m, "method")) + assert.Equal(t, q.id, mapValue(t, m, "dns.id")) + assert.Equal(t, q.opcode, mapValue(t, m, "dns.op_code")) + assert.Equal(t, q.q_class, mapValue(t, m, "dns.question.class")) + assert.Equal(t, q.q_type, mapValue(t, m, "dns.question.type")) + assert.Equal(t, q.q_name, mapValue(t, m, "dns.question.name")) +} + // Assert that the specified flags are set. func assertFlags(t testing.TB, m common.MapStr, flags []string) { for _, expected := range flags { @@ -790,370 +252,9 @@ func assertAddress(t testing.TB, expected common.IpPortTuple, endpoint interface assert.Equal(t, expected.Src_port, e.Port) } -// TCP tests - -func testTcpTuple() *common.TcpTuple { - t := &common.TcpTuple{ - Ip_length: 4, - Src_ip: net.IPv4(192, 168, 0, 1), Dst_ip: net.IPv4(192, 168, 0, 2), - Src_port: ClientPort, Dst_port: ServerPort, - } - t.ComputeHashebles() - return t -} - -// Verify that an empty packet is safely handled (no panics). -func TestParseTcp_emptyPacket(t *testing.T) { - dns := newDns(testing.Verbose()) - packet := newPacket(forward, []byte{}) - tcptuple := testTcpTuple() - private := protos.ProtocolData(new(dnsPrivateData)) - - dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) - assert.Empty(t, dns.transactions.Size(), "There should be no transactions.") - client := dns.results.(publisher.ChanClient) - close(client.Channel) - assert.Nil(t, <-client.Channel, "No result should have been published.") -} - -// Verify that a malformed packet is safely handled (no panics). -func TestParseTcp_malformedPacket(t *testing.T) { - dns := newDns(testing.Verbose()) - garbage := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13} - tcptuple := testTcpTuple() - packet := newPacket(forward, garbage) - private := protos.ProtocolData(new(dnsPrivateData)) - - dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) - assert.Empty(t, dns.transactions.Size(), "There should be no transactions.") -} - -// Verify that the lone request packet is parsed. -func TestParseTcp_requestPacket(t *testing.T) { - dns := newDns(testing.Verbose()) - packet := newPacket(forward, elasticATcp.request) - tcptuple := testTcpTuple() - private := protos.ProtocolData(new(dnsPrivateData)) - - dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) - assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") - client := dns.results.(publisher.ChanClient) - close(client.Channel) - assert.Nil(t, <-client.Channel, "No result should have been published.") -} - -// Verify that the lone response packet is parsed and that an error -// result is published. -func TestParseTcp_responseOnly(t *testing.T) { - dns := newDns(testing.Verbose()) - q := elasticATcp - packet := newPacket(reverse, q.response) - tcptuple := testTcpTuple() - private := protos.ProtocolData(new(dnsPrivateData)) - - dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) - m := expectResult(t, dns) - assert.Equal(t, "tcp", mapValue(t, m, "transport")) - assert.Nil(t, mapValue(t, m, "bytes_in")) - assert.Equal(t, len(q.response), mapValue(t, m, "bytes_out")) - assert.Nil(t, mapValue(t, m, "responsetime")) - assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) - assert.Equal(t, OrphanedResponseMsg, mapValue(t, m, "notes")) - assertMapStrData(t, m, q) -} - -// Verify that the first request is published without a response and that -// the status is error. This second packet will remain in the transaction -// map awaiting a response. -func TestParseTcp_duplicateRequests(t *testing.T) { - dns := newDns(testing.Verbose()) - q := elasticATcp - packet := newPacket(forward, q.request) - tcptuple := testTcpTuple() - private := protos.ProtocolData(new(dnsPrivateData)) - - dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) - assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") - packet = newPacket(forward, q.request) - dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) - assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") - - m := expectResult(t, dns) - assert.Equal(t, "tcp", mapValue(t, m, "transport")) - assert.Equal(t, len(q.request), mapValue(t, m, "bytes_in")) - assert.Nil(t, mapValue(t, m, "bytes_out")) - assert.Nil(t, mapValue(t, m, "responsetime")) - assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) - assert.Equal(t, DuplicateQueryMsg, mapValue(t, m, "notes")) -} - -// parseTcpRequestResponse parses a request then a response packet and validates -// the published result. -func parseTcpRequestResponse(t testing.TB, dns *Dns, q DnsTestMessage) { - packet := newPacket(forward, q.request) - tcptuple := testTcpTuple() - private := protos.ProtocolData(new(dnsPrivateData)) - dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) - - packet = newPacket(reverse, q.response) - dns.Parse(packet, tcptuple, tcp.TcpDirectionReverse, private) - - assert.Empty(t, dns.transactions.Size(), "There should be no transactions.") - - m := expectResult(t, dns) - assert.Equal(t, "tcp", mapValue(t, m, "transport")) - assert.Equal(t, len(q.request), mapValue(t, m, "bytes_in")) - assert.Equal(t, len(q.response), mapValue(t, m, "bytes_out")) - assert.NotNil(t, mapValue(t, m, "responsetime")) - - if assert.ObjectsAreEqual("NOERROR", mapValue(t, m, "dns.response_code")) { - assert.Equal(t, common.OK_STATUS, mapValue(t, m, "status")) - } else { - assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) - } - - assert.Nil(t, mapValue(t, m, "notes")) - assertMapStrData(t, m, q) -} - -// Verify that the split lone request packet is decoded. -func TestDecodeTcpSplitRequest(t *testing.T) { - stream := &DnsStream{data: sophosTxtTcp.request[:10], message: new(DnsMessage)} - _, err := decodeDnsData(TransportTcp, stream.data) - - assert.NotNil(t, err, "Not expecting a complete message yet") - - stream.data = append(stream.data, sophosTxtTcp.request[10:]...) - _, err = decodeDnsData(TransportTcp, stream.data) - - assert.Nil(t, err, "Message should be complete") -} - -// Verify that the split lone request packet is parsed. -func TestParseTcpSplitResponse(t *testing.T) { - dns := newDns(testing.Verbose()) - tcpQuery := elasticATcp - - q := tcpQuery.request - r0 := tcpQuery.response[:10] - r1 := tcpQuery.response[10:] - - tcptuple := testTcpTuple() - private := protos.ProtocolData(new(dnsPrivateData)) - - packet := newPacket(forward, q) - private = dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) - assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") - - packet = newPacket(reverse, r0) - private = dns.Parse(packet, tcptuple, tcp.TcpDirectionReverse, private) - assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") - - packet = newPacket(reverse, r1) - private = dns.Parse(packet, tcptuple, tcp.TcpDirectionReverse, private) - assert.Empty(t, dns.transactions.Size(), "There should be no transaction.") - - m := expectResult(t, dns) - assert.Equal(t, "tcp", mapValue(t, m, "transport")) - assert.Equal(t, len(tcpQuery.request), mapValue(t, m, "bytes_in")) - assert.Equal(t, len(tcpQuery.response), mapValue(t, m, "bytes_out")) - assert.NotNil(t, mapValue(t, m, "responsetime")) - - if assert.ObjectsAreEqual("NOERROR", mapValue(t, m, "dns.response_code")) { - assert.Equal(t, common.OK_STATUS, mapValue(t, m, "status")) - } else { - assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) - } - - assert.Nil(t, mapValue(t, m, "notes")) - assertMapStrData(t, m, tcpQuery) -} - -func TestGapRequestDrop(t *testing.T) { - dns := newDns(testing.Verbose()) - q := sophosTxtTcp.request[:10] - - packet := newPacket(forward, q) - tcptuple := testTcpTuple() - private := protos.ProtocolData(new(dnsPrivateData)) - - private = dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) - - private, drop := dns.GapInStream(tcptuple, tcp.TcpDirectionOriginal, 10, private) - - assert.Equal(t, true, drop) - - private = dns.ReceivedFin(tcptuple, tcp.TcpDirectionOriginal, private) - - client := dns.results.(publisher.ChanClient) - close(client.Channel) - mapStr := <-client.Channel - assert.Nil(t, mapStr, "No result should have been published.") -} - -// Verify that a gap during the response publish the request with Notes -func TestGapResponse(t *testing.T) { - dns := newDns(testing.Verbose()) - q := sophosTxtTcp.request - r := sophosTxtTcp.response[:10] - - tcptuple := testTcpTuple() - private := protos.ProtocolData(new(dnsPrivateData)) - - packet := newPacket(forward, q) - private = dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) - assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") - - packet = newPacket(reverse, r) - private = dns.Parse(packet, tcptuple, tcp.TcpDirectionReverse, private) - assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") - - private, drop := dns.GapInStream(tcptuple, tcp.TcpDirectionReverse, 10, private) - assert.Equal(t, true, drop) - - private = dns.ReceivedFin(tcptuple, tcp.TcpDirectionReverse, private) - - client := dns.results.(publisher.ChanClient) - close(client.Channel) - mapStr := <-client.Channel - assert.NotNil(t, mapStr, "One result should have been published.") - assert.Equal(t, mapStr["notes"], "Response packet's data could not be decoded as DNS.") - assert.Nil(t, mapStr["answers"]) -} - -// Verify that a gap/fin happening after a valid query create only one tansaction -func TestGapFinValidMessage(t *testing.T) { - dns := newDns(testing.Verbose()) - q := sophosTxtTcp.request - - tcptuple := testTcpTuple() - private := protos.ProtocolData(new(dnsPrivateData)) - - packet := newPacket(forward, q) - private = dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) - assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") - - private, drop := dns.GapInStream(tcptuple, tcp.TcpDirectionOriginal, 10, private) - assert.Equal(t, false, drop) - - private = dns.ReceivedFin(tcptuple, tcp.TcpDirectionReverse, private) - assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") - - client := dns.results.(publisher.ChanClient) - close(client.Channel) - mapStr := <-client.Channel - assert.Nil(t, mapStr, "No result should have been published.") - assert.Empty(t, mapStr["notes"], "There should be no notes") -} - -// Verify that a Fin during the response publish the request with Notes -func TestFinResponse(t *testing.T) { - dns := newDns(testing.Verbose()) - q := zoneAxfrTcp.request - r := zoneAxfrTcp.response[:10] - - tcptuple := testTcpTuple() - private := protos.ProtocolData(new(dnsPrivateData)) - - packet := newPacket(forward, q) - private = dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) - assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") - - packet = newPacket(reverse, r) - private = dns.Parse(packet, tcptuple, tcp.TcpDirectionReverse, private) - assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") - - private = dns.ReceivedFin(tcptuple, tcp.TcpDirectionReverse, private) - - client := dns.results.(publisher.ChanClient) - close(client.Channel) - mapStr := <-client.Channel - assert.NotNil(t, mapStr, "One result should have been published.") - assert.Equal(t, mapStr["notes"], "Response packet's data could not be decoded as DNS.") - assert.Nil(t, mapStr["answers"]) -} - -// Verify that the request/response pair are parsed and that a result -// is published. -func TestParseTcp_requestResponse(t *testing.T) { - parseTcpRequestResponse(t, newDns(testing.Verbose()), elasticATcp) -} - -// Verify all DNS TCP test messages are parsed correctly. -func TestParseTcp_allTestMessages(t *testing.T) { - dns := newDns(testing.Verbose()) - for _, q := range messagesTcp { - t.Logf("Testing with query for %s", q.q_name) - parseTcpRequestResponse(t, dns, q) - } -} - -// Benchmarks TCP parsing for the given test message. -func benchmarkTcp(b *testing.B, q DnsTestMessage) { - dns := newDns(false) - for i := 0; i < b.N; i++ { - packet := newPacket(forward, q.request) - tcptuple := testTcpTuple() - private := protos.ProtocolData(new(dnsPrivateData)) - dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) - - packet = newPacket(reverse, q.response) - tcptuple = testTcpTuple() - private = protos.ProtocolData(new(dnsPrivateData)) - dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) - - client := dns.results.(publisher.ChanClient) - <-client.Channel - } -} - -// Benchmark Tcp parsing against each test message. -func BenchmarkTcpElasticA(b *testing.B) { benchmarkTcp(b, elasticATcp) } -func BenchmarkTcpZoneIxfr(b *testing.B) { benchmarkTcp(b, zoneAxfrTcp) } -func BenchmarkTcpGithubPtr(b *testing.B) { benchmarkTcp(b, githubPtrTcp) } -func BenchmarkTcpSophosTxt(b *testing.B) { benchmarkTcp(b, sophosTxtTcp) } - -// Benchmark that runs with parallelism to help find concurrency related -// issues. To run with parallelism, the 'go test' cpu flag must be set -// greater than 1, otherwise it just runs concurrently but not in parallel. -func BenchmarkParallelTcpParse(b *testing.B) { - rand.Seed(22) - numMessages := len(messagesTcp) - dns := newDns(false) - client := dns.results.(publisher.ChanClient) - - // Drain the results channal while the test is running. - go func() { - totalMessages := 0 - for r := range client.Channel { - _ = r - totalMessages++ - } - fmt.Printf("Parsed %d messages.\n", totalMessages) - }() - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - // Each iteration parses one message, either a request or a response. - // The request and response could be parsed on different goroutines. - for pb.Next() { - q := messagesTcp[rand.Intn(numMessages)] - var packet *protos.Packet - var tcptuple *common.TcpTuple - var private protos.ProtocolData - - if rand.Intn(2) == 0 { - packet = newPacket(forward, q.request) - tcptuple = testTcpTuple() - private = protos.ProtocolData(new(dnsPrivateData)) - } else { - packet = newPacket(reverse, q.response) - tcptuple = testTcpTuple() - private = protos.ProtocolData(new(dnsPrivateData)) - } - dns.Parse(packet, tcptuple, tcp.TcpDirectionOriginal, private) - } - }) - - defer close(client.Channel) +// Verify that nameToString encodes non-printable characters. +func Test_nameToString_encodesNonPrintable(t *testing.T) { + name := "\n \r \t \" \\ \u2318.dnstunnel.com" + escapedName := "\\n \\r \\t \\\" \\\\ \\226\\140\\152.dnstunnel.com" + assert.Equal(t, escapedName, nameToString([]byte(name))) } diff --git a/packetbeat/protos/dns/dns_udp.go b/packetbeat/protos/dns/dns_udp.go new file mode 100644 index 00000000000..c14c162f348 --- /dev/null +++ b/packetbeat/protos/dns/dns_udp.go @@ -0,0 +1,39 @@ +package dns + +import ( + "github.com/elastic/beats/libbeat/logp" + + "github.com/elastic/beats/packetbeat/procs" + "github.com/elastic/beats/packetbeat/protos" +) + +func (dns *Dns) ParseUdp(pkt *protos.Packet) { + defer logp.Recover("Dns ParseUdp") + + logp.Debug("dns", "Parsing packet addressed with %s of length %d.", + pkt.Tuple.String(), len(pkt.Payload)) + + dnsPkt, err := decodeDnsData(TransportUdp, pkt.Payload) + if err != nil { + // This means that malformed requests or responses are being sent or + // that someone is attempting to the DNS port for non-DNS traffic. Both + // are issues that a monitoring system should report. + logp.Debug("dns", "%s", err.Error()) + return + } + + dnsTuple := DnsTupleFromIpPort(&pkt.Tuple, TransportUdp, dnsPkt.ID) + dnsMsg := &DnsMessage{ + Ts: pkt.Ts, + Tuple: pkt.Tuple, + CmdlineTuple: procs.ProcWatcher.FindProcessesTuple(&pkt.Tuple), + Data: dnsPkt, + Length: len(pkt.Payload), + } + + if dnsMsg.Data.QR == Query { + dns.receivedDnsRequest(&dnsTuple, dnsMsg) + } else /* Response */ { + dns.receivedDnsResponse(&dnsTuple, dnsMsg) + } +} diff --git a/packetbeat/protos/dns/dns_udp_test.go b/packetbeat/protos/dns/dns_udp_test.go new file mode 100644 index 00000000000..a6ac8749e73 --- /dev/null +++ b/packetbeat/protos/dns/dns_udp_test.go @@ -0,0 +1,408 @@ +// Unit tests and benchmarks for the dns package. +// +// The byte array test data was generated from pcap files using the gopacket +// test_creator.py script contained in the gopacket repository. The script was +// modified to drop the Ethernet, IP, and UDP headers from the byte arrays +// (skip the first 42 bytes). +// +// TODO: +// * Add test validation for responsetime to make sure unit conversion +// is being done correctly. +// * Add validation of special fields provided in MX, SOA, NS queries. +// * Add test case to verify that Include_authorities and Include_additionals +// are working. +// * Add test case for Send_request and validate the stringified DNS message. +// * Add test case for Send_response and validate the stringified DNS message. + +package dns + +import ( + "fmt" + "math/rand" + "testing" + "time" + + "github.com/elastic/beats/packetbeat/protos" + + "github.com/elastic/beats/libbeat/common" + "github.com/elastic/beats/libbeat/publisher" + "github.com/stretchr/testify/assert" + "github.com/tsg/gopacket/layers" +) + +// Verify that the interface for UDP has been satisfied. +var _ protos.UdpProtocolPlugin = &Dns{} + +// DNS messages for testing. When adding a new test message, add it to the +// messages array and create a new benchmark test for the message. +var ( + // An array of all test messages. + messages = []DnsTestMessage{ + elasticA, + zoneIxfr, + githubPtr, + sophosTxt, + } + + elasticA = DnsTestMessage{ + id: 8529, + opcode: "QUERY", + flags: []string{"rd", "ra"}, + rcode: "NOERROR", + q_class: "IN", + q_type: "A", + q_name: "elastic.co", + answers: []string{"54.148.130.30", "54.69.104.66"}, + request: []byte{ + 0x21, 0x51, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x65, 0x6c, 0x61, + 0x73, 0x74, 0x69, 0x63, 0x02, 0x63, 0x6f, 0x00, 0x00, 0x01, 0x00, 0x01, + }, + response: []byte{ + 0x21, 0x51, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x07, 0x65, 0x6c, 0x61, + 0x73, 0x74, 0x69, 0x63, 0x02, 0x63, 0x6f, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x39, 0x00, 0x04, 0x36, 0x94, 0x82, 0x1e, 0xc0, 0x0c, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x39, 0x00, 0x04, 0x36, 0x45, 0x68, 0x42, + }, + } + + zoneIxfr = DnsTestMessage{ + id: 16384, + opcode: "QUERY", + flags: []string{"ra"}, + rcode: "NOERROR", + q_class: "IN", + q_type: "IXFR", + q_name: "etas.com", + answers: []string{"training2003p", "training2003p", "training2003p", + "training2003p", "1.1.1.100"}, + request: []byte{ + 0x40, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x04, 0x65, 0x74, 0x61, + 0x73, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0xfb, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x06, 0x00, 0x01, + 0x00, 0x00, 0x0e, 0x10, 0x00, 0x2f, 0x0d, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x32, + 0x30, 0x30, 0x33, 0x70, 0x00, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x02, 0x58, 0x00, 0x01, 0x51, + 0x80, 0x00, 0x00, 0x0e, 0x10, 0x4d, 0x53, + }, + response: []byte{ + 0x40, 0x00, 0x80, 0x80, 0x00, 0x01, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x04, 0x65, 0x74, 0x61, + 0x73, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0xfb, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x06, 0x00, 0x01, + 0x00, 0x00, 0x0e, 0x10, 0x00, 0x2f, 0x0d, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x32, + 0x30, 0x30, 0x33, 0x70, 0x00, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x02, 0x58, 0x00, 0x01, 0x51, + 0x80, 0x00, 0x00, 0x0e, 0x10, 0xc0, 0x0c, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x0e, 0x10, 0x00, + 0x18, 0xc0, 0x26, 0xc0, 0x35, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x02, + 0x58, 0x00, 0x01, 0x51, 0x80, 0x00, 0x00, 0x0e, 0x10, 0xc0, 0x0c, 0x00, 0x06, 0x00, 0x01, 0x00, + 0x00, 0x0e, 0x10, 0x00, 0x18, 0xc0, 0x26, 0xc0, 0x35, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, + 0x3c, 0x00, 0x00, 0x02, 0x58, 0x00, 0x01, 0x51, 0x80, 0x00, 0x00, 0x0e, 0x10, 0x05, 0x69, 0x6e, + 0x64, 0x65, 0x78, 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x0e, 0x10, 0x00, 0x04, 0x01, + 0x01, 0x01, 0x64, 0xc0, 0x0c, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x0e, 0x10, 0x00, 0x18, 0xc0, + 0x26, 0xc0, 0x35, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x02, 0x58, 0x00, + 0x01, 0x51, 0x80, 0x00, 0x00, 0x0e, 0x10, + }, + } + + githubPtr = DnsTestMessage{ + id: 344, + opcode: "QUERY", + flags: []string{"rd", "ra"}, + rcode: "NOERROR", + q_class: "IN", + q_type: "PTR", + q_name: "131.252.30.192.in-addr.arpa", + answers: []string{"github.com"}, + authorities: []string{"a.root-servers.net", "b.root-servers.net", "c.root-servers.net", + "d.root-servers.net", "e.root-servers.net", "f.root-servers.net", "g.root-servers.net", + "h.root-servers.net", "i.root-servers.net", "j.root-servers.net", "k.root-servers.net", + "l.root-servers.net", "m.root-servers.net"}, + request: []byte{ + 0x01, 0x58, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x31, 0x33, 0x31, + 0x03, 0x32, 0x35, 0x32, 0x02, 0x33, 0x30, 0x03, 0x31, 0x39, 0x32, 0x07, 0x69, 0x6e, 0x2d, 0x61, + 0x64, 0x64, 0x72, 0x04, 0x61, 0x72, 0x70, 0x61, 0x00, 0x00, 0x0c, 0x00, 0x01, + }, + response: []byte{ + 0x01, 0x58, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x0d, 0x00, 0x00, 0x03, 0x31, 0x33, 0x31, + 0x03, 0x32, 0x35, 0x32, 0x02, 0x33, 0x30, 0x03, 0x31, 0x39, 0x32, 0x07, 0x69, 0x6e, 0x2d, 0x61, + 0x64, 0x64, 0x72, 0x04, 0x61, 0x72, 0x70, 0x61, 0x00, 0x00, 0x0c, 0x00, 0x01, 0xc0, 0x0c, 0x00, + 0x0c, 0x00, 0x01, 0x00, 0x00, 0x09, 0xe2, 0x00, 0x0c, 0x06, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x14, + 0x01, 0x6c, 0x0c, 0x72, 0x6f, 0x6f, 0x74, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x03, + 0x6e, 0x65, 0x74, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, + 0x65, 0xc0, 0x52, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x63, + 0xc0, 0x52, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x62, 0xc0, + 0x52, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x61, 0xc0, 0x52, + 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x68, 0xc0, 0x52, 0x00, + 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x66, 0xc0, 0x52, 0x00, 0x00, + 0x02, 0x00, 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x69, 0xc0, 0x52, 0x00, 0x00, 0x02, + 0x00, 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x67, 0xc0, 0x52, 0x00, 0x00, 0x02, 0x00, + 0x01, 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x6d, 0xc0, 0x52, 0x00, 0x00, 0x02, 0x00, 0x01, + 0x00, 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x64, 0xc0, 0x52, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, + 0x00, 0x07, 0xb8, 0x00, 0x04, 0x01, 0x6a, 0xc0, 0x52, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, + 0x07, 0xb8, 0x00, 0x04, 0x01, 0x6b, 0xc0, 0x52, + }, + } + + sophosTxt = DnsTestMessage{ + id: 8238, + opcode: "QUERY", + flags: []string{"rd", "ra"}, + rcode: "NXDOMAIN", + q_class: "IN", + q_type: "TXT", + q_name: "3.1o19ss00s2s17s4qp375sp49r830n2n4n923s8839052s7p7768s53365226pp3.659p1r741os37393" + + "648s2348o762q1066q53rq5p4614r1q4781qpr16n809qp4.879o3o734q9sns005o3pp76q83.2q65qns3spns" + + "1081s5rn5sr74opqrqnpq6rn3ro5.i.00.mac.sophosxl.net", + request: []byte{ + 0x20, 0x2e, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x33, 0x3f, 0x31, + 0x6f, 0x31, 0x39, 0x73, 0x73, 0x30, 0x30, 0x73, 0x32, 0x73, 0x31, 0x37, 0x73, 0x34, 0x71, 0x70, + 0x33, 0x37, 0x35, 0x73, 0x70, 0x34, 0x39, 0x72, 0x38, 0x33, 0x30, 0x6e, 0x32, 0x6e, 0x34, 0x6e, + 0x39, 0x32, 0x33, 0x73, 0x38, 0x38, 0x33, 0x39, 0x30, 0x35, 0x32, 0x73, 0x37, 0x70, 0x37, 0x37, + 0x36, 0x38, 0x73, 0x35, 0x33, 0x33, 0x36, 0x35, 0x32, 0x32, 0x36, 0x70, 0x70, 0x33, 0x3f, 0x36, + 0x35, 0x39, 0x70, 0x31, 0x72, 0x37, 0x34, 0x31, 0x6f, 0x73, 0x33, 0x37, 0x33, 0x39, 0x33, 0x36, + 0x34, 0x38, 0x73, 0x32, 0x33, 0x34, 0x38, 0x6f, 0x37, 0x36, 0x32, 0x71, 0x31, 0x30, 0x36, 0x36, + 0x71, 0x35, 0x33, 0x72, 0x71, 0x35, 0x70, 0x34, 0x36, 0x31, 0x34, 0x72, 0x31, 0x71, 0x34, 0x37, + 0x38, 0x31, 0x71, 0x70, 0x72, 0x31, 0x36, 0x6e, 0x38, 0x30, 0x39, 0x71, 0x70, 0x34, 0x1a, 0x38, + 0x37, 0x39, 0x6f, 0x33, 0x6f, 0x37, 0x33, 0x34, 0x71, 0x39, 0x73, 0x6e, 0x73, 0x30, 0x30, 0x35, + 0x6f, 0x33, 0x70, 0x70, 0x37, 0x36, 0x71, 0x38, 0x33, 0x28, 0x32, 0x71, 0x36, 0x35, 0x71, 0x6e, + 0x73, 0x33, 0x73, 0x70, 0x6e, 0x73, 0x31, 0x30, 0x38, 0x31, 0x73, 0x35, 0x72, 0x6e, 0x35, 0x73, + 0x72, 0x37, 0x34, 0x6f, 0x70, 0x71, 0x72, 0x71, 0x6e, 0x70, 0x71, 0x36, 0x72, 0x6e, 0x33, 0x72, + 0x6f, 0x35, 0x01, 0x69, 0x02, 0x30, 0x30, 0x03, 0x6d, 0x61, 0x63, 0x08, 0x73, 0x6f, 0x70, 0x68, + 0x6f, 0x73, 0x78, 0x6c, 0x03, 0x6e, 0x65, 0x74, 0x00, 0x00, 0x10, 0x00, 0x01, + }, + response: []byte{ + 0x20, 0x2e, 0x81, 0x83, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x33, 0x3f, 0x31, + 0x6f, 0x31, 0x39, 0x73, 0x73, 0x30, 0x30, 0x73, 0x32, 0x73, 0x31, 0x37, 0x73, 0x34, 0x71, 0x70, + 0x33, 0x37, 0x35, 0x73, 0x70, 0x34, 0x39, 0x72, 0x38, 0x33, 0x30, 0x6e, 0x32, 0x6e, 0x34, 0x6e, + 0x39, 0x32, 0x33, 0x73, 0x38, 0x38, 0x33, 0x39, 0x30, 0x35, 0x32, 0x73, 0x37, 0x70, 0x37, 0x37, + 0x36, 0x38, 0x73, 0x35, 0x33, 0x33, 0x36, 0x35, 0x32, 0x32, 0x36, 0x70, 0x70, 0x33, 0x3f, 0x36, + 0x35, 0x39, 0x70, 0x31, 0x72, 0x37, 0x34, 0x31, 0x6f, 0x73, 0x33, 0x37, 0x33, 0x39, 0x33, 0x36, + 0x34, 0x38, 0x73, 0x32, 0x33, 0x34, 0x38, 0x6f, 0x37, 0x36, 0x32, 0x71, 0x31, 0x30, 0x36, 0x36, + 0x71, 0x35, 0x33, 0x72, 0x71, 0x35, 0x70, 0x34, 0x36, 0x31, 0x34, 0x72, 0x31, 0x71, 0x34, 0x37, + 0x38, 0x31, 0x71, 0x70, 0x72, 0x31, 0x36, 0x6e, 0x38, 0x30, 0x39, 0x71, 0x70, 0x34, 0x1a, 0x38, + 0x37, 0x39, 0x6f, 0x33, 0x6f, 0x37, 0x33, 0x34, 0x71, 0x39, 0x73, 0x6e, 0x73, 0x30, 0x30, 0x35, + 0x6f, 0x33, 0x70, 0x70, 0x37, 0x36, 0x71, 0x38, 0x33, 0x28, 0x32, 0x71, 0x36, 0x35, 0x71, 0x6e, + 0x73, 0x33, 0x73, 0x70, 0x6e, 0x73, 0x31, 0x30, 0x38, 0x31, 0x73, 0x35, 0x72, 0x6e, 0x35, 0x73, + 0x72, 0x37, 0x34, 0x6f, 0x70, 0x71, 0x72, 0x71, 0x6e, 0x70, 0x71, 0x36, 0x72, 0x6e, 0x33, 0x72, + 0x6f, 0x35, 0x01, 0x69, 0x02, 0x30, 0x30, 0x03, 0x6d, 0x61, 0x63, 0x08, 0x73, 0x6f, 0x70, 0x68, + 0x6f, 0x73, 0x78, 0x6c, 0x03, 0x6e, 0x65, 0x74, 0x00, 0x00, 0x10, 0x00, 0x01, + }, + } +) + +// Verify that an empty packet is safely handled (no panics). +func TestParseUdp_emptyPacket(t *testing.T) { + dns := newDns(testing.Verbose()) + packet := newPacket(forward, []byte{}) + dns.ParseUdp(packet) + assert.Empty(t, dns.transactions.Size(), "There should be no transactions.") + client := dns.results.(publisher.ChanClient) + close(client.Channel) + assert.Nil(t, <-client.Channel, "No result should have been published.") +} + +// Verify that a malformed packet is safely handled (no panics). +func TestParseUdp_malformedPacket(t *testing.T) { + dns := newDns(testing.Verbose()) + garbage := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13} + packet := newPacket(forward, garbage) + dns.ParseUdp(packet) + assert.Empty(t, dns.transactions.Size(), "There should be no transactions.") + + // As a future addition, a malformed message should publish a result. +} + +// Verify that the lone request packet is parsed. +func TestParseUdp_requestPacket(t *testing.T) { + dns := newDns(testing.Verbose()) + packet := newPacket(forward, elasticA.request) + dns.ParseUdp(packet) + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + client := dns.results.(publisher.ChanClient) + close(client.Channel) + assert.Nil(t, <-client.Channel, "No result should have been published.") +} + +// Verify that the lone response packet is parsed and that an error +// result is published. +func TestParseUdp_responseOnly(t *testing.T) { + dns := newDns(testing.Verbose()) + q := elasticA + packet := newPacket(reverse, q.response) + dns.ParseUdp(packet) + + m := expectResult(t, dns) + assert.Equal(t, "udp", mapValue(t, m, "transport")) + assert.Nil(t, mapValue(t, m, "bytes_in")) + assert.Equal(t, len(q.response), mapValue(t, m, "bytes_out")) + assert.Nil(t, mapValue(t, m, "responsetime")) + assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) + assert.Equal(t, OrphanedResponse.Error(), mapValue(t, m, "notes")) + assertMapStrData(t, m, q) +} + +// Verify that the first request is published without a response and that +// the status is error. This second packet will remain in the transaction +// map awaiting a response. +func TestParseUdp_duplicateRequests(t *testing.T) { + dns := newDns(testing.Verbose()) + q := elasticA + packet := newPacket(forward, q.request) + dns.ParseUdp(packet) + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + packet = newPacket(forward, q.request) + dns.ParseUdp(packet) + assert.Equal(t, 1, dns.transactions.Size(), "There should be one transaction.") + + m := expectResult(t, dns) + assert.Equal(t, "udp", mapValue(t, m, "transport")) + assert.Equal(t, len(q.request), mapValue(t, m, "bytes_in")) + assert.Nil(t, mapValue(t, m, "bytes_out")) + assert.Nil(t, mapValue(t, m, "responsetime")) + assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) + assert.Equal(t, DuplicateQueryMsg.Error(), mapValue(t, m, "notes")) +} + +// Verify that the request/response pair are parsed and that a result +// is published. +func TestParseUdp_requestResponse(t *testing.T) { + parseUdpRequestResponse(t, newDns(testing.Verbose()), elasticA) +} + +// Verify all DNS test messages are parsed correctly. +func TestParseUdp_allTestMessages(t *testing.T) { + dns := newDns(testing.Verbose()) + for _, q := range messages { + t.Logf("Testing with query for %s", q.q_name) + parseUdpRequestResponse(t, dns, q) + } +} + +// Verify that expireTransaction publishes an event with an error status +// and note. +func TestExpireTransaction(t *testing.T) { + dns := newDns(testing.Verbose()) + + trans := newTransaction(time.Now(), DnsTuple{}, common.CmdlineTuple{}) + trans.Request = &DnsMessage{ + Data: &layers.DNS{ + Questions: []layers.DNSQuestion{{}}, + }, + } + dns.expireTransaction(trans) + + m := expectResult(t, dns) + assert.Nil(t, mapValue(t, m, "bytes_out")) + assert.Nil(t, mapValue(t, m, "responsetime")) + assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) + assert.Equal(t, NoResponse.Error(), mapValue(t, m, "notes")) +} + +// Verify that an empty DNS request packet can be published. +func TestPublishTransaction_emptyDnsRequest(t *testing.T) { + dns := newDns(testing.Verbose()) + + trans := newTransaction(time.Now(), DnsTuple{}, common.CmdlineTuple{}) + trans.Request = &DnsMessage{ + Data: &layers.DNS{}, + } + dns.publishTransaction(trans) + + m := expectResult(t, dns) + assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) +} + +// Verify that an empty DNS response packet can be published. +func TestPublishTransaction_emptyDnsResponse(t *testing.T) { + dns := newDns(testing.Verbose()) + + trans := newTransaction(time.Now(), DnsTuple{}, common.CmdlineTuple{}) + trans.Response = &DnsMessage{ + Data: &layers.DNS{}, + } + dns.publishTransaction(trans) + + m := expectResult(t, dns) + assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) +} + +// Benchmarks UDP parsing for the given test message. +func benchmarkUdp(b *testing.B, q DnsTestMessage) { + dns := newDns(false) + for i := 0; i < b.N; i++ { + packet := newPacket(forward, q.request) + dns.ParseUdp(packet) + packet = newPacket(reverse, q.response) + dns.ParseUdp(packet) + + client := dns.results.(publisher.ChanClient) + <-client.Channel + } +} + +// Benchmark UDP parsing against each test message. +func BenchmarkUdpElasticA(b *testing.B) { benchmarkUdp(b, elasticA) } +func BenchmarkUdpZoneIxfr(b *testing.B) { benchmarkUdp(b, zoneIxfr) } +func BenchmarkUdpGithubPtr(b *testing.B) { benchmarkUdp(b, githubPtr) } +func BenchmarkUdpSophosTxt(b *testing.B) { benchmarkUdp(b, sophosTxt) } + +// Benchmark that runs with parallelism to help find concurrency related +// issues. To run with parallelism, the 'go test' cpu flag must be set +// greater than 1, otherwise it just runs concurrently but not in parallel. +func BenchmarkParallelUdpParse(b *testing.B) { + rand.Seed(22) + numMessages := len(messages) + dns := newDns(false) + client := dns.results.(publisher.ChanClient) + + // Drain the results channal while the test is running. + go func() { + totalMessages := 0 + for r := range client.Channel { + _ = r + totalMessages++ + } + fmt.Printf("Parsed %d messages.\n", totalMessages) + }() + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + // Each iteration parses one message, either a request or a response. + // The request and response could be parsed on different goroutines. + for pb.Next() { + q := messages[rand.Intn(numMessages)] + var packet *protos.Packet + if rand.Intn(2) == 0 { + packet = newPacket(forward, q.request) + } else { + packet = newPacket(reverse, q.response) + } + dns.ParseUdp(packet) + } + }) + + defer close(client.Channel) +} + +// parseUdpRequestResponse parses a request then a response packet and validates +// the published result. +func parseUdpRequestResponse(t testing.TB, dns *Dns, q DnsTestMessage) { + packet := newPacket(forward, q.request) + dns.ParseUdp(packet) + packet = newPacket(reverse, q.response) + dns.ParseUdp(packet) + assert.Empty(t, dns.transactions.Size(), "There should be no transactions.") + + m := expectResult(t, dns) + assert.Equal(t, "udp", mapValue(t, m, "transport")) + assert.Equal(t, len(q.request), mapValue(t, m, "bytes_in")) + assert.Equal(t, len(q.response), mapValue(t, m, "bytes_out")) + assert.NotNil(t, mapValue(t, m, "responsetime")) + + if assert.ObjectsAreEqual("NOERROR", mapValue(t, m, "dns.response_code")) { + assert.Equal(t, common.OK_STATUS, mapValue(t, m, "status")) + } else { + assert.Equal(t, common.ERROR_STATUS, mapValue(t, m, "status")) + } + + assert.Nil(t, mapValue(t, m, "notes")) + assertMapStrData(t, m, q) +} diff --git a/packetbeat/protos/dns/errors.go b/packetbeat/protos/dns/errors.go new file mode 100644 index 00000000000..735c99abafe --- /dev/null +++ b/packetbeat/protos/dns/errors.go @@ -0,0 +1,39 @@ +package dns + +// All dns protocol errors are defined here. + +type Error interface { + error + ResponseError() string +} + +type DNSError struct { + Err string +} + +func (e *DNSError) Error() string { + if e == nil { + return "" + } + return e.Err +} + +func (e *DNSError) ResponseError() string { + return "Response: " + e.Error() +} + +// Messages +var ( + NonDnsMsg = &DNSError{Err: "Message's data could not be decoded as DNS"} + ZeroLengthMsg = &DNSError{Err: "Message's length was set to zero"} + UnexpectedLengthMsg = &DNSError{Err: "Unexpected message data length"} + DuplicateQueryMsg = &DNSError{Err: "Another query with the same DNS ID from this client " + + "was received so this query was closed without receiving a response"} + IncompleteMsg = &DNSError{Err: "Message's data is incomplete"} + NoResponse = &DNSError{Err: "No response to this query was received"} +) + +// TCP responses +var ( + OrphanedResponse = &DNSError{Err: "Response: received without an associated Query"} +) diff --git a/tests/system/pcaps/dns_tcp_axfr.pcap b/tests/system/pcaps/dns_tcp_axfr.pcap deleted file mode 100644 index 0291cbf166d..00000000000 Binary files a/tests/system/pcaps/dns_tcp_axfr.pcap and /dev/null differ