Skip to content

Commit

Permalink
[pkg/ottl] Add "NetworkDirection" converter
Browse files Browse the repository at this point in the history
  • Loading branch information
michalpristas committed Nov 28, 2024
1 parent 9a52558 commit ad18e1a
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 0 deletions.
6 changes: 6 additions & 0 deletions pkg/ottl/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,12 @@ func Test_e2e_converters(t *testing.T) {
tCtx.GetLogRecord().Attributes().PutInt("test", 1000000)
},
},
{
statement: `set(attributes["test"], NetworkDirection("192.168.1.1", "192.168.1.2", ["private"]))`,
want: func(tCtx ottllog.TransformContext) {
tCtx.GetLogRecord().Attributes().PutStr("test", "internal")
},
},
{
statement: `set(attributes["test"], "pass") where Now() - Now() < Duration("1h")`,
want: func(tCtx ottllog.TransformContext) {
Expand Down
27 changes: 27 additions & 0 deletions pkg/ottl/ottlfuncs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ Available Converters:
- [Minutes](#minutes)
- [Month](#month)
- [Nanoseconds](#nanoseconds)
- [NetworkDirection](#networkdirection)
- [Now](#now)
- [ParseCSV](#parsecsv)
- [ParseJSON](#parsejson)
Expand Down Expand Up @@ -1239,6 +1240,32 @@ Examples:

- `Nanoseconds(Duration("1h"))`

### NetworkDirection

`NetworkDirection(sourceIP, destinationIP, Optional[[]internal_networks])`

`NetworkDirection` function calculates the network direction given a source IP address, destination IP address, and a list of internal networks.
Returned value is one of following strings: `internal`,`external`,`inbound`,`outbound`.

`sourceIP` is a getter that returns string, `destinationIP` is a getter that returns string and `internal_networks` is an optional array of strings.

The named ranges supported for the `internal_networks` option are:

- `loopback` - Matches loopback addresses in the range of 127.0.0.0/8 or ::1/128.
- `unicast` or `global_unicast` - Matches global unicast addresses defined in RFC 1122, RFC 4632, and RFC 4291 with the exception of the IPv4 broadcast address (255.255.255.255). This includes private address ranges.
- `multicast` - Matches multicast addresses.
- `interface_local_multicast` - Matches IPv6 interface-local multicast addresses.
- `link_local_unicast` - Matches link-local unicast addresses.
- `link_local_multicast` - Matches link-local multicast addresses.
- `private` - Matches private address ranges defined in RFC 1918 (IPv4) and RFC 4193 (IPv6).
- `public` - Matches addresses that are not loopback, unspecified, IPv4 broadcast, link local unicast, link local multicast, interface local multicast, or private.
- `unspecified` - Matches unspecified addresses (either the IPv4 address "0.0.0.0" or the IPv6 address "::").

- `NetworkDirection("192.168.1.1", "192.168.1.2", ["private"])`
- `NetworkDirection("192.168.1.1", "192.168.1.2", ["public"])`
- `NetworkDirection("0.0.0.0", "0.0.0.0", ["unspecified"])`
- `NetworkDirection("192.168.1.1", "10.0.1.1", ["10.0.0.0/8"])`

### Now

`Now()`
Expand Down
188 changes: 188 additions & 0 deletions pkg/ottl/ottlfuncs/func_network_direction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package ottlfuncs // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs"

import (
"context"
"fmt"
"net"

"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
)

const (
// direction
DIRECTION_INTERNAL = "internal"
DIRECTION_EXTERNAL = "external"
DIRECTION_INBOUND = "inbound"
DIRECTION_OUTBOUND = "outbound"

// netwroks
LOOPBACK_NAMED_NETWORK = "loopback"
GLOBAL_UNICAST_NAMED_NETWORK = "global_unicast"
UNICAST_NAMED_NETWORK = "unicast"
LINK_LOCAL_UNICAST_NAMED_NETWORK = "link_local_unicast"
INTERFACE_LOCAL_NAMED_NETWORK = "interface_local_multicast"
LINK_LOCAL_MULTICAST_NAMED_NETWORK = "link_local_multicast"
MULTICAST_NAMED_NETWORK = "multicast"
UNSPECIFIED_NAMED_NETWORK = "unspecified"
PRIVATE_NAMED_NETWORK = "private"
PUBLIC_NAMED_NETWORK = "public"
)

type NetworkDirectionArguments[K any] struct {
SourceIP ottl.StringGetter[K]
DestinationIP ottl.StringGetter[K]
InternalNetworks ottl.Optional[[]string]
}

func NewNetworkDirectionFactory[K any]() ottl.Factory[K] {
return ottl.NewFactory("NetworkDirection", &NetworkDirectionArguments[K]{}, createNetworkDirectionFunction[K])
}

func createNetworkDirectionFunction[K any](_ ottl.FunctionContext, oArgs ottl.Arguments) (ottl.ExprFunc[K], error) {
args, ok := oArgs.(*NetworkDirectionArguments[K])
if !ok {
return nil, fmt.Errorf("URLFactory args must be of type *NetworkDirectionArguments[K]")
}

return networkDirection(args.SourceIP, args.DestinationIP, args.InternalNetworks), nil //revive:disable-line:var-naming
}

func networkDirection[K any](sourceIP ottl.StringGetter[K], destinationIP ottl.StringGetter[K], internalNetworksOptional ottl.Optional[[]string]) ottl.ExprFunc[K] { //revive:disable-line:var-naming
var internalNetworks []string
if !internalNetworksOptional.IsEmpty() {
for _, network := range internalNetworksOptional.Get() {
if len(network) > 0 {
internalNetworks = append(internalNetworks, network)
}
}
}

return func(ctx context.Context, tCtx K) (any, error) {
sourceIPString, err := sourceIP.Get(ctx, tCtx)
if err != nil {
return nil, fmt.Errorf("failed parsing source IP: %w", err)
}

if sourceIPString == "" {
return nil, fmt.Errorf("source IP cannot be empty")
}

destinationIPString, err := destinationIP.Get(ctx, tCtx)
if err != nil {
return nil, fmt.Errorf("failed parsing destination IP: %w", err)
}

if destinationIPString == "" {
return nil, fmt.Errorf("destination IP cannot be empty")
}

sourceAddress := net.ParseIP(sourceIPString)
if sourceAddress == nil {
return nil, fmt.Errorf("source IP %q is not a valid address", sourceIPString)
}

sourceInternal, err := isInternalIP(sourceAddress, internalNetworks)
if err != nil {
return nil, fmt.Errorf("failed determining whether source IP is internal: %w", err)
}

destinationAddress := net.ParseIP(destinationIPString)
if destinationAddress == nil {
return nil, fmt.Errorf("destination IP %q is not a valid address", destinationIPString)
}

destinationInternal, err := isInternalIP(destinationAddress, internalNetworks)
if err != nil {
return nil, fmt.Errorf("failed determining whether destination IP is internal: %w", err)
}

if sourceInternal && destinationInternal {
return DIRECTION_INTERNAL, nil
}
if sourceInternal {
return DIRECTION_OUTBOUND, nil
}
if destinationInternal {
return DIRECTION_INBOUND, nil
}
return DIRECTION_EXTERNAL, nil
}
}

func isInternalIP(addr net.IP, networks []string) (bool, error) {
for _, network := range networks {
inNetwork, err := isIPInNetwork(addr, network)
if err != nil {
return false, err
}

if inNetwork {
return true, nil
}
}
return false, nil
}

func isIPInNetwork(addr net.IP, network string) (bool, error) {
switch network {
case LOOPBACK_NAMED_NETWORK:
return addr.IsLoopback(), nil
case GLOBAL_UNICAST_NAMED_NETWORK:
return addr.IsGlobalUnicast(), nil
case LINK_LOCAL_UNICAST_NAMED_NETWORK:
return addr.IsLinkLocalUnicast(), nil
case LINK_LOCAL_MULTICAST_NAMED_NETWORK:
return addr.IsLinkLocalMulticast(), nil
case INTERFACE_LOCAL_NAMED_NETWORK:
return addr.IsInterfaceLocalMulticast(), nil
case MULTICAST_NAMED_NETWORK:
return addr.IsMulticast(), nil
case PRIVATE_NAMED_NETWORK:
return isPrivateNetwork(addr), nil
case PUBLIC_NAMED_NETWORK:
return isPublicNetwork(addr), nil
case UNSPECIFIED_NAMED_NETWORK:
return addr.IsUnspecified(), nil

}

// cidr range
return isInRange(addr, network)
}

func isPrivateNetwork(addr net.IP) bool {
isAddrInRange, _ := isInRange(addr, "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "fd00::/8")
return isAddrInRange
}

func isPublicNetwork(addr net.IP) bool {
if isPrivateNetwork(addr) ||
addr.IsLoopback() ||
addr.IsUnspecified() ||
addr.IsLinkLocalUnicast() ||
addr.IsLinkLocalMulticast() ||
addr.IsInterfaceLocalMulticast() ||
addr.Equal(net.IPv4bcast) {
return false
}

return false
}

func isInRange(addr net.IP, networks ...string) (bool, error) {
for _, network := range networks {
_, mask, err := net.ParseCIDR(network)
if err != nil {
return false, fmt.Errorf("invalid network definition for %q", network)
}

if mask.Contains(addr) {
return true, nil
}
}

return false, nil
}
75 changes: 75 additions & 0 deletions pkg/ottl/ottlfuncs/func_network_direction_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package ottlfuncs

import (
"context"
"fmt"
"testing"

"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
"github.com/stretchr/testify/assert"
)

func Test_NetworkDirection(t *testing.T) {
testCases := []struct {
sourceIP string
destinationIP string
internalNetworks []string
expectedDirection string
expectedError string
}{
// cidr range tests
{"10.0.1.1", "192.168.1.2", []string{"10.0.0.0/8"}, "outbound", ""},
{"192.168.1.2", "10.0.1.1", []string{"10.0.0.0/8"}, "inbound", ""},

// private network tests
{"192.168.1.1", "192.168.1.2", []string{"private"}, "internal", ""},
{"10.0.1.1", "192.168.1.2", []string{"private"}, "internal", ""},
{"192.168.1.1", "172.16.0.1", []string{"private"}, "internal", ""},
{"192.168.1.1", "fd12:3456:789a:1::1", []string{"private"}, "internal", ""},

// public network tests
{"192.168.1.1", "192.168.1.2", []string{"public"}, "external", ""},
{"10.0.1.1", "192.168.1.2", []string{"public"}, "external", ""},
{"192.168.1.1", "172.16.0.1", []string{"public"}, "external", ""},
{"192.168.1.1", "fd12:3456:789a:1::1", []string{"public"}, "external", ""},

// unspecified tests
{"0.0.0.0", "0.0.0.0", []string{"unspecified"}, "internal", ""},
{"::", "::", []string{"unspecified"}, "internal", ""},

// invalid inputs tests
{"invalid", "192.168.1.2", []string{}, "", `source IP "invalid" is not a valid address`},
{"192.168.1.1", "invalid", []string{}, "", `destination IP "invalid" is not a valid address`},
{"192.168.1.1", "192.168.1.2", []string{"10.0.0.0/8", "invalid"}, "", `failed determining whether source IP is internal: invalid network definition for "invalid"`},
}

for i, testCase := range testCases {
t.Run(fmt.Sprintf("test case #%d: %s -> %s", i, testCase.sourceIP, testCase.destinationIP), func(t *testing.T) {
internalNetworksOptional := ottl.NewTestingOptional[[]string](testCase.internalNetworks)

source := &ottl.StandardStringGetter[any]{
Getter: func(_ context.Context, _ any) (any, error) {
return testCase.sourceIP, nil
},
}

destination := &ottl.StandardStringGetter[any]{
Getter: func(_ context.Context, _ any) (any, error) {
return testCase.destinationIP, nil
},
}
exprFunc := networkDirection(source, destination, internalNetworksOptional)
result, err := exprFunc(context.Background(), nil)
if len(testCase.expectedError) > 0 {
assert.Error(t, err)
assert.Equal(t, testCase.expectedError, err.Error())
} else {
assert.NoError(t, err)
assert.Equal(t, testCase.expectedDirection, result)
}
})
}
}
1 change: 1 addition & 0 deletions pkg/ottl/ottlfuncs/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ func converters[K any]() []ottl.Factory[K] {
NewMinutesFactory[K](),
NewMonthFactory[K](),
NewNanosecondsFactory[K](),
NewNetworkDirectionFactory[K](),
NewNowFactory[K](),
NewParseCSVFactory[K](),
NewParseJSONFactory[K](),
Expand Down

0 comments on commit ad18e1a

Please sign in to comment.