364 lines
9.1 KiB
Go
364 lines
9.1 KiB
Go
// Licensed to Elasticsearch B.V. under one or more contributor
|
|
// license agreements. See the NOTICE file distributed with
|
|
// this work for additional information regarding copyright
|
|
// ownership. Elasticsearch B.V. licenses this file to you under
|
|
// the Apache License, Version 2.0 (the "License"); you may
|
|
// not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
package icmp
|
|
|
|
import (
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/elastic/beats/libbeat/beat"
|
|
"github.com/elastic/beats/libbeat/common"
|
|
"github.com/elastic/beats/libbeat/logp"
|
|
"github.com/elastic/beats/libbeat/monitoring"
|
|
|
|
"github.com/elastic/beats/packetbeat/flows"
|
|
"github.com/elastic/beats/packetbeat/protos"
|
|
|
|
"github.com/tsg/gopacket/layers"
|
|
)
|
|
|
|
type icmpPlugin struct {
|
|
sendRequest bool
|
|
sendResponse bool
|
|
|
|
localIps []net.IP
|
|
|
|
// Active ICMP transactions.
|
|
// The map key is the hashableIcmpTuple associated with the request.
|
|
transactions *common.Cache
|
|
transactionTimeout time.Duration
|
|
|
|
results protos.Reporter
|
|
}
|
|
|
|
type ICMPv4Processor interface {
|
|
ProcessICMPv4(flowID *flows.FlowID, hdr *layers.ICMPv4, pkt *protos.Packet)
|
|
}
|
|
|
|
type ICMPv6Processor interface {
|
|
ProcessICMPv6(flowID *flows.FlowID, hdr *layers.ICMPv6, pkt *protos.Packet)
|
|
}
|
|
|
|
const (
|
|
directionLocalOnly = iota
|
|
directionFromInside
|
|
directionFromOutside
|
|
)
|
|
|
|
// Notes that are added to messages during exceptional conditions.
|
|
const (
|
|
duplicateRequestMsg = "Another request with the same Id and Seq was received so this request was closed without receiving a response."
|
|
orphanedRequestMsg = "Request was received without an associated response."
|
|
orphanedResponseMsg = "Response was received without an associated request."
|
|
)
|
|
|
|
var (
|
|
unmatchedRequests = monitoring.NewInt(nil, "icmp.unmatched_requests")
|
|
unmatchedResponses = monitoring.NewInt(nil, "icmp.unmatched_responses")
|
|
duplicateRequests = monitoring.NewInt(nil, "icmp.duplicate_requests")
|
|
)
|
|
|
|
func New(testMode bool, results protos.Reporter, cfg *common.Config) (*icmpPlugin, error) {
|
|
p := &icmpPlugin{}
|
|
config := defaultConfig
|
|
if !testMode {
|
|
if err := cfg.Unpack(&config); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if err := p.init(results, &config); err != nil {
|
|
return nil, err
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func (icmp *icmpPlugin) init(results protos.Reporter, config *icmpConfig) error {
|
|
icmp.setFromConfig(config)
|
|
|
|
var err error
|
|
icmp.localIps, err = common.LocalIPAddrs()
|
|
if err != nil {
|
|
logp.Err("icmp", "Error getting local IP addresses: %s", err)
|
|
icmp.localIps = []net.IP{}
|
|
}
|
|
logp.Debug("icmp", "Local IP addresses: %s", icmp.localIps)
|
|
|
|
var removalListener = func(k common.Key, v common.Value) {
|
|
icmp.expireTransaction(k.(hashableIcmpTuple), v.(*icmpTransaction))
|
|
}
|
|
|
|
icmp.transactions = common.NewCacheWithRemovalListener(
|
|
icmp.transactionTimeout,
|
|
protos.DefaultTransactionHashSize,
|
|
removalListener)
|
|
|
|
icmp.transactions.StartJanitor(icmp.transactionTimeout)
|
|
|
|
icmp.results = results
|
|
|
|
return nil
|
|
}
|
|
|
|
func (icmp *icmpPlugin) setFromConfig(config *icmpConfig) {
|
|
icmp.sendRequest = config.SendRequest
|
|
icmp.sendResponse = config.SendResponse
|
|
icmp.transactionTimeout = config.TransactionTimeout
|
|
}
|
|
|
|
func (icmp *icmpPlugin) ProcessICMPv4(
|
|
flowID *flows.FlowID,
|
|
icmp4 *layers.ICMPv4,
|
|
pkt *protos.Packet,
|
|
) {
|
|
typ := uint8(icmp4.TypeCode >> 8)
|
|
code := uint8(icmp4.TypeCode)
|
|
id, seq := extractTrackingData(4, typ, &icmp4.BaseLayer)
|
|
|
|
tuple := &icmpTuple{
|
|
icmpVersion: 4,
|
|
srcIP: pkt.Tuple.SrcIP,
|
|
dstIP: pkt.Tuple.DstIP,
|
|
id: id,
|
|
seq: seq,
|
|
}
|
|
msg := &icmpMessage{
|
|
ts: pkt.Ts,
|
|
Type: typ,
|
|
code: code,
|
|
length: len(icmp4.BaseLayer.Payload),
|
|
}
|
|
|
|
if isRequest(tuple, msg) {
|
|
if flowID != nil {
|
|
flowID.AddICMPv4Request(id)
|
|
}
|
|
icmp.processRequest(tuple, msg)
|
|
} else {
|
|
if flowID != nil {
|
|
flowID.AddICMPv4Response(id)
|
|
}
|
|
icmp.processResponse(tuple, msg)
|
|
}
|
|
}
|
|
|
|
func (icmp *icmpPlugin) ProcessICMPv6(
|
|
flowID *flows.FlowID,
|
|
icmp6 *layers.ICMPv6,
|
|
pkt *protos.Packet,
|
|
) {
|
|
typ := uint8(icmp6.TypeCode >> 8)
|
|
code := uint8(icmp6.TypeCode)
|
|
id, seq := extractTrackingData(6, typ, &icmp6.BaseLayer)
|
|
tuple := &icmpTuple{
|
|
icmpVersion: 6,
|
|
srcIP: pkt.Tuple.SrcIP,
|
|
dstIP: pkt.Tuple.DstIP,
|
|
id: id,
|
|
seq: seq,
|
|
}
|
|
msg := &icmpMessage{
|
|
ts: pkt.Ts,
|
|
Type: typ,
|
|
code: code,
|
|
length: len(icmp6.BaseLayer.Payload),
|
|
}
|
|
|
|
if isRequest(tuple, msg) {
|
|
if flowID != nil {
|
|
flowID.AddICMPv6Request(id)
|
|
}
|
|
icmp.processRequest(tuple, msg)
|
|
} else {
|
|
if flowID != nil {
|
|
flowID.AddICMPv6Response(id)
|
|
}
|
|
icmp.processResponse(tuple, msg)
|
|
}
|
|
}
|
|
|
|
func (icmp *icmpPlugin) processRequest(tuple *icmpTuple, msg *icmpMessage) {
|
|
logp.Debug("icmp", "Processing request. %s", tuple)
|
|
|
|
trans := icmp.deleteTransaction(tuple.Hashable())
|
|
if trans != nil {
|
|
trans.notes = append(trans.notes, duplicateRequestMsg)
|
|
logp.Debug("icmp", duplicateRequestMsg+" %s", tuple)
|
|
duplicateRequests.Add(1)
|
|
icmp.publishTransaction(trans)
|
|
}
|
|
|
|
trans = &icmpTransaction{ts: msg.ts, tuple: *tuple}
|
|
trans.request = msg
|
|
|
|
if requiresCounterpart(tuple, msg) {
|
|
icmp.transactions.Put(tuple.Hashable(), trans)
|
|
} else {
|
|
icmp.publishTransaction(trans)
|
|
}
|
|
}
|
|
|
|
func (icmp *icmpPlugin) processResponse(tuple *icmpTuple, msg *icmpMessage) {
|
|
logp.Debug("icmp", "Processing response. %s", tuple)
|
|
|
|
revTuple := tuple.Reverse()
|
|
trans := icmp.deleteTransaction(revTuple.Hashable())
|
|
if trans == nil {
|
|
trans = &icmpTransaction{ts: msg.ts, tuple: revTuple}
|
|
trans.notes = append(trans.notes, orphanedResponseMsg)
|
|
logp.Debug("icmp", orphanedResponseMsg+" %s", tuple)
|
|
unmatchedResponses.Add(1)
|
|
}
|
|
|
|
trans.response = msg
|
|
icmp.publishTransaction(trans)
|
|
}
|
|
|
|
func (icmp *icmpPlugin) direction(t *icmpTransaction) uint8 {
|
|
if !icmp.isLocalIP(t.tuple.srcIP) {
|
|
return directionFromOutside
|
|
}
|
|
if !icmp.isLocalIP(t.tuple.dstIP) {
|
|
return directionFromInside
|
|
}
|
|
return directionLocalOnly
|
|
}
|
|
|
|
func (icmp *icmpPlugin) isLocalIP(ip net.IP) bool {
|
|
if ip.IsLoopback() {
|
|
return true
|
|
}
|
|
|
|
for _, localIP := range icmp.localIps {
|
|
if ip.Equal(localIP) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (icmp *icmpPlugin) getTransaction(k hashableIcmpTuple) *icmpTransaction {
|
|
v := icmp.transactions.Get(k)
|
|
if v != nil {
|
|
return v.(*icmpTransaction)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (icmp *icmpPlugin) deleteTransaction(k hashableIcmpTuple) *icmpTransaction {
|
|
v := icmp.transactions.Delete(k)
|
|
if v != nil {
|
|
return v.(*icmpTransaction)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (icmp *icmpPlugin) expireTransaction(tuple hashableIcmpTuple, trans *icmpTransaction) {
|
|
trans.notes = append(trans.notes, orphanedRequestMsg)
|
|
logp.Debug("icmp", orphanedRequestMsg+" %s", &trans.tuple)
|
|
unmatchedRequests.Add(1)
|
|
icmp.publishTransaction(trans)
|
|
}
|
|
|
|
func (icmp *icmpPlugin) publishTransaction(trans *icmpTransaction) {
|
|
if icmp.results == nil {
|
|
return
|
|
}
|
|
|
|
logp.Debug("icmp", "Publishing transaction. %s", &trans.tuple)
|
|
|
|
fields := common.MapStr{}
|
|
|
|
// common fields - group "env"
|
|
fields["client_ip"] = trans.tuple.srcIP
|
|
fields["ip"] = trans.tuple.dstIP
|
|
|
|
// common fields - group "event"
|
|
fields["type"] = "icmp" // protocol name
|
|
fields["path"] = trans.tuple.dstIP // what is requested (dst ip)
|
|
if trans.HasError() {
|
|
fields["status"] = common.ERROR_STATUS
|
|
} else {
|
|
fields["status"] = common.OK_STATUS
|
|
}
|
|
if len(trans.notes) > 0 {
|
|
fields["notes"] = trans.notes
|
|
}
|
|
|
|
// common fields - group "measurements"
|
|
responsetime, hasResponseTime := trans.ResponseTimeMillis()
|
|
if hasResponseTime {
|
|
fields["responsetime"] = responsetime
|
|
}
|
|
switch icmp.direction(trans) {
|
|
case directionFromInside:
|
|
if trans.request != nil {
|
|
fields["bytes_out"] = trans.request.length
|
|
}
|
|
if trans.response != nil {
|
|
fields["bytes_in"] = trans.response.length
|
|
}
|
|
case directionFromOutside:
|
|
if trans.request != nil {
|
|
fields["bytes_in"] = trans.request.length
|
|
}
|
|
if trans.response != nil {
|
|
fields["bytes_out"] = trans.response.length
|
|
}
|
|
}
|
|
|
|
// event fields - group "icmp"
|
|
icmpEvent := common.MapStr{}
|
|
fields["icmp"] = icmpEvent
|
|
|
|
icmpEvent["version"] = trans.tuple.icmpVersion
|
|
|
|
if trans.request != nil {
|
|
request := common.MapStr{}
|
|
icmpEvent["request"] = request
|
|
|
|
request["message"] = humanReadable(&trans.tuple, trans.request)
|
|
request["type"] = trans.request.Type
|
|
request["code"] = trans.request.code
|
|
|
|
// TODO: Add more info. The IPv4/IPv6 payload could be interesting.
|
|
// if icmp.SendRequest {
|
|
// request["payload"] = ""
|
|
// }
|
|
}
|
|
|
|
if trans.response != nil {
|
|
response := common.MapStr{}
|
|
icmpEvent["response"] = response
|
|
|
|
response["message"] = humanReadable(&trans.tuple, trans.response)
|
|
response["type"] = trans.response.Type
|
|
response["code"] = trans.response.code
|
|
|
|
// TODO: Add more info. The IPv4/IPv6 payload could be interesting.
|
|
// if icmp.SendResponse {
|
|
// response["payload"] = ""
|
|
// }
|
|
}
|
|
|
|
icmp.results(beat.Event{
|
|
Timestamp: trans.ts,
|
|
Fields: fields,
|
|
})
|
|
}
|