Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DHCP protocol handling for packetbeat #7359

Closed
wants to merge 10 commits into from

Conversation

brianwaskiewicz
Copy link
Contributor

First at contributing to packetbeat; this adds support for parsing DHCP messages.

@elasticmachine
Copy link
Collaborator

Since this is a community submitted pull request, a Jenkins build has not been kicked off automatically. Can an Elastic organization member please verify the contents of this patch and then kick off a build manually?

protos.Register("dhcp", New)
}

func New(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exported function New should have comment or be unexported

protos.Register("dhcp", New)
}

// Creates and returns a new dhcpPlugin.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment on exported function New should be of the form "New ..."

@andrewkroh
Copy link
Member

Thanks for submitting this pull request.

The PR is currently failing the initial CI smoke tests because the code needs to be formatted. If you run make fmt and make update that should hopefully fix things.

Secondly when adding a new protocol we also like to have some "system tests" in place that execute Packetbeat against a PCAP file containing DHCP traffic. These tests should perform some assertions on the output events. It's like end-to-end test for Packetbeat. Those tests are contained in packetbeat/tests/system. You can use one of the existing tests, such as dns, as an example to guide you.

@brianwaskiewicz
Copy link
Contributor Author

brianwaskiewicz commented Jun 20, 2018 via email

@andrewkroh
Copy link
Member

If you run make update that should regenerate those files and include your new DHCP content.

@andrewkroh
Copy link
Member

jenkins test this

@andrewkroh andrewkroh requested a review from adriansr July 17, 2018 22:22
Copy link
Contributor

@adriansr adriansr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your contribution!

There are a few issues I'd like to see improved, have a look at the comments I left.

type: group
fields:
- name: transaction_id
type: string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string is not a valid type in Elasticsearch, this must be keyword.

for idx < len(payload) {
result = append(result, []byte{payload[idx], payload[idx+1], payload[idx+2], payload[idx+3]})
idx += 4
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this function much. It's unnecessary allocation that is happening for every DHCP packet.
All the uses of this function's result can be replaced by slicing pkt.Payload:
rows[3] => pkt.Payload[12:16]
combineRows(rows,11,16) => pkt.Payload[44:108]

Also the bounds check is wrong and will cause a panic for a payload whose size is not a multiple of 4.
for idx + 4 <= len(payload) or similar should do.

func bytesToIPv4(bytes []byte) string {
ip := net.IPv4(bytes[0], bytes[1], bytes[2], bytes[3])
str := ip.String()
if str == "0.0.0.0" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of comparing with "0.0.0.0" string, use ip.IsUnspecified()


func trimNullBytes(b []byte) []byte {
index := bytes.Index(b, []byte{0})
return b[0:index]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will panic if for some reason the byte 0 is not found within the slice. You need to validate the result of bytes.Index.

Nit: use bytes.IndexByte


func (dhcp *dhcpPlugin) parsePacket(pkt *protos.Packet) beat.Event {
dhcpFields := make(map[string]interface{})
rows := payloadToRows(pkt.Payload)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function needs bounds checking, make sure that Payload has the expected length.

hwaddr := combineRows(rows, 7, 4)
dhcpFields["client_hwaddr"] = bytesToHardwareAddress(hwaddr, int(pkt.Payload[2]))
serverName := trimNullBytes(combineRows(rows, 11, 16))
dhcpFields["server_name"] = string(serverName)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this "server_name" actually used in DHCP or is it a BOOTP leftover? I couldn't find much information about it and in my captures is always empty. I suggest you don't set the field at all in dhcpFields if empty.

idx = len(options)
} else {
idx++
remaining = int(options[idx])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing bounds check here, it can panic on a malformed or crafted packet.

idx++
}
return result
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as just using payload[240:], don't need a function for this unless there is a point in copying the slice, which you can do with copy(...)

}
} else {
body = append(body, options[idx])
remaining--
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function can be improved to read the option at once instead of looping and appending every byte:

remaining = int(options[idx])
if idx + remaining + 1 > len(options) {
   error...
}
result[option] = options[idx+1:idx+1+remaining]

As before, I'm not sure if it's necessary to copy into a new slice or not.

@adriansr
Copy link
Contributor

I'm worried that this protocol can create a lot of noise on a busy network.

What do you think about adding a config option to only report dhcp messages based on their type. Something like:

report_types: [ 'DHCPACK', 'DHCPNACK' ]

So that only those two types get reported.

Also I'm not 100% sure which events would be interesting. Do ACK and NACK contain all the information (IP assignments) and do all transactions end in ACK or NACK?

@andrewkroh
Copy link
Member

andrewkroh commented Jul 19, 2018

For transacted protocols like this, where there is a request/response and transaction ID to link the two packets, Packetbeat usually combines the whole transaction into a single event.

I think we should keep it as you have it for DHCP and not combine the request and responses. There are scenarios in DHCP where a client sends only a single message, like a DHCPDECLINE, and does not expect a response. It would be simpler to not have to put any protocol state knowledge into the code.

Cons of Combining Request/Response

  • The src and dest IPs used in the two packets of the transaction are not the same. The client uses src 0.0.0.0 and broadcasts to 255.255.255.255. And the server responds to client's assigned address.
  • A request might receive multiple responses if there is more than one DHCP server. Handling this would be messy in the schema (arrays of objects).

Pros

  • With transactions we can detect error scenarios where the client never received a response and indicate this is the event. However this can be handled in other ways such as with a flag in the request event or by use Watcher.
  • We can compute a response time.

Copy link
Member

@andrewkroh andrewkroh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main packetbeat config files also need to be updated to include the dhcp proto. Those are in packetbeat/_meta/beat*yml.

}

var (
defaultConfig = dhcpConfig{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be initialized with the default ports.

	defaultConfig = dhcpConfig{
		ProtocolCommon: config.ProtocolCommon{
			Ports: []int{67, 68},
		},
	}

}

func (dhcp *dhcpPlugin) ParseUDP(pkt *protos.Packet) {
event := dhcp.parsePacket(pkt)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be some error handling here. If the data isn't actually DHCP (like there aren't enough bytes make a valid DHCP packet) then an event should not be sent.

type: string
description: The IP address of the DHCP server that the client should use for the next step in the bootstrap process.

- name: gateway_ip
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a better name for this would be relay_ip.

type: string
description: The type of hardware used for the local network (Ethernet, LocalTalk, etc)

- name: message_type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think message_type, server_identifier, and subnet_mask should be prefixed with option. to highlight these are options in the DHCP message.

Timestamp: pkt.Ts,
Fields: map[string]interface{}{
"transport": "udp",
"ip": pkt.Tuple.DstIP.String(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that the ip/port and client_ip/client_port values need to be swapped for the BOOTREPLY packets in order or accurately represent the role of the two hosts in the exchange.

dhcpFields := make(map[string]interface{})
rows := payloadToRows(pkt.Payload)
dhcpFields["transaction_id"] = hex.EncodeToString(rows[1])
dhcpFields["client_ip"] = bytesToIPv4(rows[3])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several of these IPs may be unspecified. We should omit them if they are unset in the message.

dhcpFields["client_hwaddr"] = bytesToHardwareAddress(hwaddr, int(pkt.Payload[2]))
serverName := trimNullBytes(combineRows(rows, 11, 16))
dhcpFields["server_name"] = string(serverName)
dhcpFields["op_code"] = int(pkt.Payload[0])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to render this value as a string like BootRequest and BootReply.

@andrewkroh
Copy link
Member

I think it would make sense to call this dhcpv4 because we will likely want to add a dhcpv6 in the future. And dhcpv6 would be a separate protocol operating on separate ports.

@andrewkroh
Copy link
Member

@brianwaskiewicz I expanded on your pull request in #7647. I tried a third-party library for the parsing.

@andrewkroh
Copy link
Member

DHCP support has been merged in #7647.

@brianwaskiewicz, you're listed as a co-author in the commit. Thanks!

@andrewkroh andrewkroh closed this Aug 13, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants