-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Conversation
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? |
packetbeat/protos/dhcp/dhcp.go
Outdated
protos.Register("dhcp", New) | ||
} | ||
|
||
func New( |
There was a problem hiding this comment.
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
packetbeat/protos/dhcp/dhcp.go
Outdated
protos.Register("dhcp", New) | ||
} | ||
|
||
// Creates and returns a new dhcpPlugin. |
There was a problem hiding this comment.
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 ..."
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 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. |
@andrewkroh Added a test and fixed the formatting. I think the make check is failing because the fields.go and fields.asciidoc need to be updated. Any pointers on the best way to do that?
…________________________________
From: Andrew Kroh <[email protected]>
Sent: Tuesday, June 19, 2018 6:24:57 AM
To: elastic/beats
Cc: brianwaskiewicz; Author
Subject: Re: [elastic/beats] Add DHCP protocol handling for packetbeat (#7359)
Thanks for submitting this pull request.
The PR is currently failing the initial CI smoke tests because the code needs to be formatted<https://travis-ci.org/elastic/beats/jobs/393907452#L592-L594>. 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<https://github.com/elastic/beats/tree/master/packetbeat/tests/system>. You can use one of the existing tests, such as dns<https://github.com/elastic/beats/blob/master/packetbeat/tests/system/test_0032_dns.py>, as an example to guide you.
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub<#7359 (comment)>, or mute the thread<https://github.com/notifications/unsubscribe-auth/AJbqn59C0ZxXtGHNjx738UPEff3G6jpiks5t-PupgaJpZM4UstEw>.
|
If you run |
jenkins test this |
There was a problem hiding this 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 |
There was a problem hiding this comment.
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 | ||
} |
There was a problem hiding this comment.
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" { |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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]) |
There was a problem hiding this comment.
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 | ||
} |
There was a problem hiding this comment.
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-- |
There was a problem hiding this comment.
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.
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? |
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
Pros
|
There was a problem hiding this 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{} |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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(), |
There was a problem hiding this comment.
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]) |
There was a problem hiding this comment.
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]) |
There was a problem hiding this comment.
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.
I think it would make sense to call this |
@brianwaskiewicz I expanded on your pull request in #7647. I tried a third-party library for the parsing. |
DHCP support has been merged in #7647. @brianwaskiewicz, you're listed as a co-author in the commit. Thanks! |
First at contributing to packetbeat; this adds support for parsing DHCP messages.