Skip to content

Commit

Permalink
[NETPATH-371] Move common functions to separate package, create separ…
Browse files Browse the repository at this point in the history
…ate testutils package (#31819)
  • Loading branch information
ken-schneider authored Dec 10, 2024
1 parent 0b42e2b commit 385f25a
Show file tree
Hide file tree
Showing 14 changed files with 481 additions and 399 deletions.
160 changes: 160 additions & 0 deletions pkg/networkpath/traceroute/common/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.

// Package common contains common functionality for both TCP and UDP
// traceroute implementations
package common

import (
"fmt"
"net"
"strconv"
"time"

"github.com/DataDog/datadog-agent/pkg/util/log"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"golang.org/x/net/ipv4"
)

const (
// IPProtoICMP is the IP protocol number for ICMP
// we create our own constant here because there are
// different imports for the constant in different
// operating systems
IPProtoICMP = 1
)

type (
// Results encapsulates a response from the
// traceroute
Results struct {
Source net.IP
SourcePort uint16
Target net.IP
DstPort uint16
Hops []*Hop
}

// Hop encapsulates information about a single
// hop in a traceroute
Hop struct {
IP net.IP
Port uint16
ICMPType layers.ICMPv4TypeCode
RTT time.Duration
IsDest bool
}

// CanceledError is sent when a listener
// is canceled
CanceledError string

// ICMPResponse encapsulates the data from
// an ICMP response packet needed for matching
ICMPResponse struct {
SrcIP net.IP
DstIP net.IP
TypeCode layers.ICMPv4TypeCode
InnerSrcIP net.IP
InnerDstIP net.IP
InnerSrcPort uint16
InnerDstPort uint16
InnerSeqNum uint32
}
)

func (c CanceledError) Error() string {
return string(c)
}

// LocalAddrForHost takes in a destionation IP and port and returns the local
// address that should be used to connect to the destination
func LocalAddrForHost(destIP net.IP, destPort uint16) (*net.UDPAddr, error) {
// this is a quick way to get the local address for connecting to the host
// using UDP as the network type to avoid actually creating a connection to
// the host, just get the OS to give us a local IP and local ephemeral port
conn, err := net.Dial("udp4", net.JoinHostPort(destIP.String(), strconv.Itoa(int(destPort))))
if err != nil {
return nil, err
}
defer conn.Close()
localAddr := conn.LocalAddr()

localUDPAddr, ok := localAddr.(*net.UDPAddr)
if !ok {
return nil, fmt.Errorf("invalid address type for %s: want %T, got %T", localAddr, localUDPAddr, localAddr)
}

return localUDPAddr, nil
}

// ParseICMP takes in an IPv4 header and payload and tries to convert to an ICMP
// message, it returns all the fields from the packet we need to validate it's the response
// we're looking for
func ParseICMP(header *ipv4.Header, payload []byte) (*ICMPResponse, error) {
// in addition to parsing, it is probably not a bad idea to do some validation
// so we can ignore the ICMP packets we don't care about
icmpResponse := ICMPResponse{}

if header.Protocol != IPProtoICMP || header.Version != 4 ||
header.Src == nil || header.Dst == nil {
return nil, fmt.Errorf("invalid IP header for ICMP packet: %+v", header)
}
icmpResponse.SrcIP = header.Src
icmpResponse.DstIP = header.Dst

var icmpv4Layer layers.ICMPv4
decoded := []gopacket.LayerType{}
icmpParser := gopacket.NewDecodingLayerParser(layers.LayerTypeICMPv4, &icmpv4Layer)
icmpParser.IgnoreUnsupported = true // ignore unsupported layers, we will decode them in the next step
if err := icmpParser.DecodeLayers(payload, &decoded); err != nil {
return nil, fmt.Errorf("failed to decode ICMP packet: %w", err)
}
// since we ignore unsupported layers, we need to check if we actually decoded
// anything
if len(decoded) < 1 {
return nil, fmt.Errorf("failed to decode ICMP packet, no layers decoded")
}
icmpResponse.TypeCode = icmpv4Layer.TypeCode

var icmpPayload []byte
if len(icmpv4Layer.Payload) < 40 {
log.Tracef("Payload length %d is less than 40, extending...\n", len(icmpv4Layer.Payload))
icmpPayload = make([]byte, 40)
copy(icmpPayload, icmpv4Layer.Payload)
// we have to set this in order for the TCP
// parser to work
icmpPayload[32] = 5 << 4 // set data offset
} else {
icmpPayload = icmpv4Layer.Payload
}

// a separate parser is needed to decode the inner IP and TCP headers because
// gopacket doesn't support this type of nesting in a single decoder
var innerIPLayer layers.IPv4
var innerTCPLayer layers.TCP
innerIPParser := gopacket.NewDecodingLayerParser(layers.LayerTypeIPv4, &innerIPLayer, &innerTCPLayer)
if err := innerIPParser.DecodeLayers(icmpPayload, &decoded); err != nil {
return nil, fmt.Errorf("failed to decode inner ICMP payload: %w", err)
}
icmpResponse.InnerSrcIP = innerIPLayer.SrcIP
icmpResponse.InnerDstIP = innerIPLayer.DstIP
icmpResponse.InnerSrcPort = uint16(innerTCPLayer.SrcPort)
icmpResponse.InnerDstPort = uint16(innerTCPLayer.DstPort)
icmpResponse.InnerSeqNum = innerTCPLayer.Seq

return &icmpResponse, nil
}

// ICMPMatch checks if an ICMP response matches the expected response
// based on the local and remote IP, port, and sequence number
func ICMPMatch(localIP net.IP, localPort uint16, remoteIP net.IP, remotePort uint16, seqNum uint32, response *ICMPResponse) bool {
return localIP.Equal(response.InnerSrcIP) &&
remoteIP.Equal(response.InnerDstIP) &&
localPort == response.InnerSrcPort &&
remotePort == response.InnerDstPort &&
seqNum == response.InnerSeqNum
}
117 changes: 117 additions & 0 deletions pkg/networkpath/traceroute/common/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.

//go:build test

package common

import (
"net"
"testing"

"github.com/DataDog/datadog-agent/pkg/networkpath/traceroute/testutils"
"github.com/google/gopacket/layers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/ipv4"
)

var (
srcIP = net.ParseIP("1.2.3.4")
dstIP = net.ParseIP("5.6.7.8")

innerSrcIP = net.ParseIP("10.0.0.1")
innerDstIP = net.ParseIP("192.168.1.1")
)

func Test_parseICMP(t *testing.T) {
ipv4Header := testutils.CreateMockIPv4Header(srcIP, dstIP, 1)
icmpLayer := testutils.CreateMockICMPLayer(layers.ICMPv4CodeTTLExceeded)
innerIPv4Layer := testutils.CreateMockIPv4Layer(innerSrcIP, innerDstIP, layers.IPProtocolTCP)
innerTCPLayer := testutils.CreateMockTCPLayer(12345, 443, 28394, 12737, true, true, true)

tt := []struct {
description string
inHeader *ipv4.Header
inPayload []byte
expected *ICMPResponse
errMsg string
}{
{
description: "empty IPv4 layer should return an error",
inHeader: &ipv4.Header{},
inPayload: []byte{},
expected: nil,
errMsg: "invalid IP header for ICMP packet",
},
{
description: "missing ICMP layer should return an error",
inHeader: ipv4Header,
inPayload: []byte{},
expected: nil,
errMsg: "failed to decode ICMP packet",
},
{
description: "missing inner layers should return an error",
inHeader: ipv4Header,
inPayload: testutils.CreateMockICMPPacket(nil, icmpLayer, nil, nil, false),
expected: nil,
errMsg: "failed to decode inner ICMP payload",
},
{
description: "ICMP packet with partial TCP header should create icmpResponse",
inHeader: ipv4Header,
inPayload: testutils.CreateMockICMPPacket(nil, icmpLayer, innerIPv4Layer, innerTCPLayer, true),
expected: &ICMPResponse{
SrcIP: srcIP,
DstIP: dstIP,
InnerSrcIP: innerSrcIP,
InnerDstIP: innerDstIP,
InnerSrcPort: 12345,
InnerDstPort: 443,
InnerSeqNum: 28394,
},
errMsg: "",
},
{
description: "full ICMP packet should create icmpResponse",
inHeader: ipv4Header,
inPayload: testutils.CreateMockICMPPacket(nil, icmpLayer, innerIPv4Layer, innerTCPLayer, true),
expected: &ICMPResponse{
SrcIP: srcIP,
DstIP: dstIP,
InnerSrcIP: innerSrcIP,
InnerDstIP: innerDstIP,
InnerSrcPort: 12345,
InnerDstPort: 443,
InnerSeqNum: 28394,
},
errMsg: "",
},
}

for _, test := range tt {
t.Run(test.description, func(t *testing.T) {
actual, err := ParseICMP(test.inHeader, test.inPayload)
if test.errMsg != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), test.errMsg)
assert.Nil(t, actual)
return
}
require.Nil(t, err)
require.NotNil(t, actual)
// assert.Equal doesn't handle net.IP well
assert.Equal(t, testutils.StructFieldCount(test.expected), testutils.StructFieldCount(actual))
assert.Truef(t, test.expected.SrcIP.Equal(actual.SrcIP), "mismatch source IPs: expected %s, got %s", test.expected.SrcIP.String(), actual.SrcIP.String())
assert.Truef(t, test.expected.DstIP.Equal(actual.DstIP), "mismatch dest IPs: expected %s, got %s", test.expected.DstIP.String(), actual.DstIP.String())
assert.Truef(t, test.expected.InnerSrcIP.Equal(actual.InnerSrcIP), "mismatch inner source IPs: expected %s, got %s", test.expected.InnerSrcIP.String(), actual.InnerSrcIP.String())
assert.Truef(t, test.expected.InnerDstIP.Equal(actual.InnerDstIP), "mismatch inner dest IPs: expected %s, got %s", test.expected.InnerDstIP.String(), actual.InnerDstIP.String())
assert.Equal(t, test.expected.InnerSrcPort, actual.InnerSrcPort)
assert.Equal(t, test.expected.InnerDstPort, actual.InnerDstPort)
assert.Equal(t, test.expected.InnerSeqNum, actual.InnerSeqNum)
})
}
}
3 changes: 2 additions & 1 deletion pkg/networkpath/traceroute/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/DataDog/datadog-agent/pkg/config/setup"
"github.com/DataDog/datadog-agent/pkg/network"
"github.com/DataDog/datadog-agent/pkg/networkpath/payload"
"github.com/DataDog/datadog-agent/pkg/networkpath/traceroute/common"
"github.com/DataDog/datadog-agent/pkg/networkpath/traceroute/config"
"github.com/DataDog/datadog-agent/pkg/networkpath/traceroute/tcp"
"github.com/DataDog/datadog-agent/pkg/process/util"
Expand Down Expand Up @@ -222,7 +223,7 @@ func (r *Runner) runTCP(cfg config.Config, hname string, target net.IP, maxTTL u
return pathResult, nil
}

func (r *Runner) processTCPResults(res *tcp.Results, hname string, destinationHost string, destinationPort uint16, destinationIP net.IP) (payload.NetworkPath, error) {
func (r *Runner) processTCPResults(res *common.Results, hname string, destinationHost string, destinationPort uint16, destinationIP net.IP) (payload.NetworkPath, error) {
traceroutePath := payload.NetworkPath{
AgentVersion: version.AgentVersion,
PathtraceID: payload.NewPathtraceID(),
Expand Down
22 changes: 0 additions & 22 deletions pkg/networkpath/traceroute/tcp/tcpv4.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ package tcp
import (
"net"
"time"

"github.com/google/gopacket/layers"
)

type (
Expand All @@ -27,26 +25,6 @@ type (
Delay time.Duration // delay between sending packets (not applicable if we go the serial send/receive route)
Timeout time.Duration // full timeout for all packets
}

// Results encapsulates a response from the TCP
// traceroute
Results struct {
Source net.IP
SourcePort uint16
Target net.IP
DstPort uint16
Hops []*Hop
}

// Hop encapsulates information about a single
// hop in a TCP traceroute
Hop struct {
IP net.IP
Port uint16
ICMPType layers.ICMPv4TypeCode
RTT time.Duration
IsDest bool
}
)

// Close doesn't to anything yet, but we should
Expand Down
13 changes: 7 additions & 6 deletions pkg/networkpath/traceroute/tcp/tcpv4_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ import (

"golang.org/x/net/ipv4"

"github.com/DataDog/datadog-agent/pkg/networkpath/traceroute/common"
"github.com/DataDog/datadog-agent/pkg/util/log"
)

// TracerouteSequential runs a traceroute sequentially where a packet is
// sent and we wait for a response before sending the next packet
func (t *TCPv4) TracerouteSequential() (*Results, error) {
func (t *TCPv4) TracerouteSequential() (*common.Results, error) {
// Get local address for the interface that connects to this
// host and store in in the probe
addr, err := localAddrForHost(t.Target, t.DestPort)
addr, err := common.LocalAddrForHost(t.Target, t.DestPort)
if err != nil {
return nil, fmt.Errorf("failed to get local address for target: %w", err)
}
Expand Down Expand Up @@ -71,7 +72,7 @@ func (t *TCPv4) TracerouteSequential() (*Results, error) {
}

// hops should be of length # of hops
hops := make([]*Hop, 0, t.MaxTTL-t.MinTTL)
hops := make([]*common.Hop, 0, t.MaxTTL-t.MinTTL)

for i := int(t.MinTTL); i <= int(t.MaxTTL); i++ {
seqNumber := rand.Uint32()
Expand All @@ -88,7 +89,7 @@ func (t *TCPv4) TracerouteSequential() (*Results, error) {
}
}

return &Results{
return &common.Results{
Source: t.srcIP,
SourcePort: t.srcPort,
Target: t.Target,
Expand All @@ -97,7 +98,7 @@ func (t *TCPv4) TracerouteSequential() (*Results, error) {
}, nil
}

func (t *TCPv4) sendAndReceive(rawIcmpConn *ipv4.RawConn, rawTCPConn *ipv4.RawConn, ttl int, seqNum uint32, timeout time.Duration) (*Hop, error) {
func (t *TCPv4) sendAndReceive(rawIcmpConn *ipv4.RawConn, rawTCPConn *ipv4.RawConn, ttl int, seqNum uint32, timeout time.Duration) (*common.Hop, error) {
tcpHeader, tcpPacket, err := createRawTCPSyn(t.srcIP, t.srcPort, t.Target, t.DestPort, seqNum, ttl)
if err != nil {
log.Errorf("failed to create TCP packet with TTL: %d, error: %s", ttl, err.Error())
Expand All @@ -122,7 +123,7 @@ func (t *TCPv4) sendAndReceive(rawIcmpConn *ipv4.RawConn, rawTCPConn *ipv4.RawCo
rtt = end.Sub(start)
}

return &Hop{
return &common.Hop{
IP: hopIP,
Port: hopPort,
ICMPType: icmpType,
Expand Down
Loading

0 comments on commit 385f25a

Please sign in to comment.