-
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
Changes from 1 commit
5e36e85
e80125f
ac054a9
03f9f21
88a3cf9
bf01234
7616f17
d4172e7
d9f14ad
6dc1256
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
- key: dhcp | ||
title: "DHCP" | ||
description: DHCP-specific event fields | ||
fields: | ||
- name: dhcp | ||
type: group | ||
fields: | ||
- name: transaction_id | ||
type: string | ||
description: Generated by the client, allows the client to correctly match requests and responses. | ||
|
||
- name: client_ip | ||
type: string | ||
description: The current IP address of the client. | ||
|
||
- name: assigned_ip | ||
type: string | ||
description: The IP address that the DHCP server is assigning to the client. | ||
|
||
- name: server_ip | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. I think a better name for this would be |
||
type: string | ||
description: The gateway IP address used by the client to contact the server. | ||
|
||
- name: client_hwaddr | ||
type: string | ||
description: The client's hardware (layer two) address. | ||
|
||
- name: server_name | ||
type: string | ||
description: Optional, for DHCPOFFER or DHCPACK messages, the name of the server sending the message. | ||
|
||
- name: op_code | ||
type: int | ||
description: The general type of DHCP message. | ||
|
||
- name: hops | ||
type: int | ||
description: The number of hops the DHCP message went through. | ||
|
||
- name: hardware_type | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. I think |
||
type: string | ||
description: The specific type of DHCP message being sent (DHCPOFFER, DCHPREQUEST, etc) | ||
|
||
- name: server_identifier | ||
type: string | ||
description: IP address of the individual DHCP server which handled this message. | ||
|
||
- name: subnet_mask | ||
type: string | ||
description: The subnet mask that the client should use on the currnet network. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package dhcp | ||
|
||
import ( | ||
"github.com/elastic/beats/packetbeat/config" | ||
) | ||
|
||
type dhcpConfig struct { | ||
config.ProtocolCommon `config:",inline"` | ||
} | ||
|
||
var ( | ||
defaultConfig = dhcpConfig{} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be initialized with the default ports.
|
||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
package dhcp | ||
|
||
import ( | ||
"bytes" | ||
"encoding/hex" | ||
"github.com/elastic/beats/libbeat/beat" | ||
"github.com/elastic/beats/libbeat/common" | ||
"github.com/elastic/beats/libbeat/logp" | ||
"github.com/elastic/beats/packetbeat/protos" | ||
"net" | ||
) | ||
|
||
var hardwareTypes = map[int]string{ | ||
1: "ethernet", | ||
6: "ieee_802", | ||
7: "arcnet", | ||
11: "localtalk", | ||
12: "localnet", | ||
14: "smds", | ||
15: "frame_relay", | ||
16: "atm", | ||
17: "hdlc", | ||
18: "fibre_channel", | ||
19: "atm", | ||
20: "serial_line", | ||
} | ||
|
||
var messageTypes = []string{ | ||
"DHCPDISCOVER", | ||
"DHCPOFFER", | ||
"DHCPREQUEST", | ||
"DHCPDECLINE", | ||
"DHCPACK", | ||
"DHCPNAK", | ||
"DHCPRELEASE", | ||
"DHCPINFORM", | ||
} | ||
|
||
type dhcpPlugin struct { | ||
ports []int | ||
results protos.Reporter | ||
} | ||
|
||
func (dhcp *dhcpPlugin) init(results protos.Reporter, config *dhcpConfig) { | ||
dhcp.ports = config.Ports | ||
dhcp.results = results | ||
} | ||
|
||
func (dhcp *dhcpPlugin) GetPorts() []int { | ||
return dhcp.ports | ||
} | ||
|
||
func payloadToRows(payload []byte) [][]byte { | ||
result := make([][]byte, 0) | ||
var idx = 0 | ||
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 commentThe 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. Also the bounds check is wrong and will cause a panic for a payload whose size is not a multiple of 4. |
||
return result | ||
} | ||
|
||
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 commentThe reason will be displayed to describe this comment to others. Learn more. Instead of comparing with "0.0.0.0" string, use ip.IsUnspecified() |
||
return "" | ||
} | ||
return str | ||
} | ||
|
||
func bytesToHardwareAddress(bytes []byte, length int) string { | ||
return net.HardwareAddr(bytes[0:length]).String() | ||
} | ||
|
||
func combineRows(rows [][]byte, start int, length int) []byte { | ||
combined := make([]byte, 0) | ||
end := start + length | ||
var idx = start | ||
for idx < end { | ||
combined = append(combined, rows[idx]...) | ||
idx++ | ||
} | ||
return combined | ||
} | ||
|
||
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 commentThe 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 extractOptions(payload []byte) []byte { | ||
result := make([]byte, 0) | ||
var idx = 240 | ||
for idx < len(payload) { | ||
result = append(result, payload[idx]) | ||
idx++ | ||
} | ||
return result | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(...) |
||
|
||
func parseOptions(options []byte) map[int][]byte { | ||
result := make(map[int][]byte) | ||
var remaining = 0 | ||
var option = 0 | ||
var body = make([]byte, 0) | ||
var idx = 0 | ||
for idx < len(options) { | ||
if remaining == 0 { | ||
if option != 0 { | ||
result[option] = body | ||
body = make([]byte, 0) | ||
} | ||
option = int(options[idx]) | ||
if option == 0 || option == 255 { | ||
idx = len(options) | ||
} else { | ||
idx++ | ||
remaining = int(options[idx]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
} | ||
} else { | ||
body = append(body, options[idx]) | ||
remaining-- | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
As before, I'm not sure if it's necessary to copy into a new slice or not. |
||
} | ||
idx++ | ||
} | ||
return result | ||
} | ||
|
||
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 commentThe 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. |
||
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 commentThe 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["assigned_ip"] = bytesToIPv4(rows[4]) | ||
dhcpFields["server_ip"] = bytesToIPv4(rows[5]) | ||
dhcpFields["gateway_ip"] = bytesToIPv4(rows[6]) | ||
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 commentThe 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. |
||
dhcpFields["op_code"] = int(pkt.Payload[0]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
dhcpFields["hops"] = int(pkt.Payload[3]) | ||
dhcpFields["hardware_type"] = hardwareTypes[int(pkt.Payload[1])] | ||
options := extractOptions(pkt.Payload) | ||
parsedOptions := parseOptions(options) | ||
if parsedOptions[53] != nil { | ||
dhcpFields["message_type"] = messageTypes[int(parsedOptions[53][0])-1] | ||
} | ||
if parsedOptions[54] != nil { | ||
dhcpFields["server_identifier"] = bytesToIPv4(parsedOptions[54]) | ||
} | ||
if parsedOptions[1] != nil { | ||
dhcpFields["subnet_mask"] = bytesToIPv4(parsedOptions[1]) | ||
} | ||
event := beat.Event{ | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. I believe that the |
||
"client_ip": pkt.Tuple.SrcIP.String(), | ||
"port": pkt.Tuple.DstPort, | ||
"client_port": pkt.Tuple.SrcPort, | ||
"type": "dhcp", | ||
"dhcp": dhcpFields, | ||
}, | ||
} | ||
return event | ||
} | ||
|
||
func (dhcp *dhcpPlugin) ParseUDP(pkt *protos.Packet) { | ||
event := dhcp.parsePacket(pkt) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
dhcp.results(event) | ||
} | ||
|
||
func init() { | ||
protos.Register("dhcp", New) | ||
} | ||
|
||
func New( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. exported function New should have comment or be unexported |
||
testMode bool, | ||
results protos.Reporter, | ||
cfg *common.Config, | ||
) (protos.Plugin, error) { | ||
p := &dhcpPlugin{} | ||
config := defaultConfig | ||
if !testMode { | ||
if err := cfg.Unpack(&config); err != nil { | ||
logp.Err("Error unpacking configuration: %s", err) | ||
return nil, err | ||
} | ||
} | ||
p.init(results, &config) | ||
return p, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package dhcp | ||
|
||
import ( | ||
"fmt" | ||
"github.com/elastic/beats/libbeat/beat" | ||
"github.com/elastic/beats/libbeat/common" | ||
"github.com/elastic/beats/packetbeat/protos" | ||
"github.com/stretchr/testify/assert" | ||
"net" | ||
"testing" | ||
"time" | ||
) | ||
|
||
var ( | ||
payload = []byte{ | ||
0x02, 0x01, 0x06, 0x00, 0x00, 0x00, 0x3d, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xa8, 0x00, 0x0a, | ||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x82, 0x01, 0xfc, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63, 0x82, 0x53, 0x63, | ||
0x35, 0x01, 0x05, 0x3a, 0x04, 0x00, 0x00, 0x07, 0x08, 0x3b, 0x04, 0x00, 0x00, 0x0c, 0x4e, 0x33, 0x04, 0x00, 0x00, 0x0e, | ||
0x10, 0x36, 0x04, 0xc0, 0xa8, 0x00, 0x01, 0x01, 0x04, 0xff, 0xff, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
} | ||
) | ||
|
||
var _ protos.UDPPlugin = &dhcpPlugin{} | ||
|
||
func get(event beat.Event, fieldName string) interface{} { | ||
value, _ := event.Fields.GetValue(fieldName) | ||
return value | ||
} | ||
|
||
func TestParsePacket(t *testing.T) { | ||
plugin := dhcpPlugin{} | ||
ipTuple := common.NewIPPortTuple(4, net.IP{192, 168, 0, 10}, 67, net.IP{192, 168, 0, 1}, 68) | ||
pkt := &protos.Packet{ | ||
Ts: time.Now(), | ||
Tuple: ipTuple, | ||
Payload: payload, | ||
} | ||
expectedDhcpFields := map[string]interface{}{ | ||
"client_ip": "", | ||
"client_hwaddr": "00:0b:82:01:fc:42", | ||
"gateway_ip": "", | ||
"hardware_type": "ethernet", | ||
"hops": int(0), | ||
"message_type": "DHCPACK", | ||
"op_code": int(2), | ||
"server_identifier": "192.168.0.1", | ||
"server_ip": "", | ||
"server_name": "", | ||
"subnet_mask": "255.255.255.0", | ||
"transaction_id": "00003d1e", | ||
"assigned_ip": "192.168.0.10", | ||
} | ||
event := plugin.parsePacket(pkt) | ||
fmt.Printf("Gpt event: %+v", event) | ||
assert.Equal(t, "192.168.0.1", get(event, "ip").(string), "Wrong ip") | ||
assert.Equal(t, "192.168.0.10", get(event, "client_ip").(string), "Wrong client_ip") | ||
dhcpFields := get(event, "dhcp").(map[string]interface{}) | ||
assert.Equal(t, expectedDhcpFields, dhcpFields) | ||
} |
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 bekeyword
.