diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e064da5150a159..a66b485924570b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -218,7 +218,6 @@ /comp/otelcol @DataDog/opentelemetry /comp/process @DataDog/processes /comp/remote-config @DataDog/remote-config -/comp/snmptraps @DataDog/network-device-monitoring /comp/systray @DataDog/windows-agent /comp/trace @DataDog/agent-apm /comp/checks/agentcrashdetect @DataDog/windows-kernel-integrations diff --git a/cmd/agent/subcommands/run/command.go b/cmd/agent/subcommands/run/command.go index fd7c2eb3628993..c129437be0b3aa 100644 --- a/cmd/agent/subcommands/run/command.go +++ b/cmd/agent/subcommands/run/command.go @@ -35,7 +35,6 @@ import ( "github.com/DataDog/datadog-agent/cmd/manager" "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexer" netflowServer "github.com/DataDog/datadog-agent/comp/netflow/server" - snmptrapsServer "github.com/DataDog/datadog-agent/comp/snmptraps/server" // checks implemented as components @@ -66,7 +65,6 @@ import ( "github.com/DataDog/datadog-agent/comp/otelcol" otelcollector "github.com/DataDog/datadog-agent/comp/otelcol/collector" "github.com/DataDog/datadog-agent/comp/remote-config/rcclient" - "github.com/DataDog/datadog-agent/comp/snmptraps" "github.com/DataDog/datadog-agent/pkg/aggregator" "github.com/DataDog/datadog-agent/pkg/api/healthprobe" "github.com/DataDog/datadog-agent/pkg/autodiscovery/providers" @@ -80,6 +78,7 @@ import ( pkgMetadata "github.com/DataDog/datadog-agent/pkg/metadata" "github.com/DataDog/datadog-agent/pkg/pidfile" "github.com/DataDog/datadog-agent/pkg/serializer" + "github.com/DataDog/datadog-agent/pkg/snmp/traps" "github.com/DataDog/datadog-agent/pkg/status" "github.com/DataDog/datadog-agent/pkg/status/health" pkgTelemetry "github.com/DataDog/datadog-agent/pkg/telemetry" @@ -198,7 +197,6 @@ func run(log log.Component, hostMetadata host.Component, invAgent inventoryagent.Component, _ netflowServer.Component, - _ snmptrapsServer.Component, _ langDetectionCl.Component, ) error { defer func() { @@ -308,7 +306,6 @@ func getSharedFxOption() fx.Option { }), ndmtmp.Bundle, netflow.Bundle, - snmptraps.Bundle, ) } @@ -511,6 +508,14 @@ func startAgent( pkgTelemetry.RegisterStatsSender(sender) } + // Start SNMP trap server + if traps.IsEnabled(pkgconfig.Datadog) { + err = traps.StartServer(hostnameDetected, demultiplexer, pkgconfig.Datadog, log) + if err != nil { + log.Errorf("Failed to start snmp-traps server: %s", err) + } + } + // Detect Cloud Provider go cloudproviders.DetectCloudProvider(context.Background()) @@ -590,6 +595,7 @@ func stopAgent(cliParams *cliParams, server dogstatsdServer.Component, demultipl if common.MetadataScheduler != nil { common.MetadataScheduler.Stop() } + traps.StopServer() api.StopServer() clcrunnerapi.StopCLCRunnerServer() jmx.StopJmxfetch() diff --git a/cmd/agent/subcommands/run/command_windows.go b/cmd/agent/subcommands/run/command_windows.go index dcb5eb1052f6c3..739018adfe3cfc 100644 --- a/cmd/agent/subcommands/run/command_windows.go +++ b/cmd/agent/subcommands/run/command_windows.go @@ -23,7 +23,6 @@ import ( "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexer" "github.com/DataDog/datadog-agent/comp/checks/agentcrashdetect" "github.com/DataDog/datadog-agent/comp/checks/agentcrashdetect/agentcrashdetectimpl" - trapserver "github.com/DataDog/datadog-agent/comp/snmptraps/server" comptraceconfig "github.com/DataDog/datadog-agent/comp/trace/config" // core components @@ -83,7 +82,6 @@ func StartAgentWithDefaults(ctxChan <-chan context.Context) (<-chan error, error hostMetadata host.Component, invAgent inventoryagent.Component, _ netflowServer.Component, - _ trapserver.Component, ) error { defer StopAgentWithDefaults(server, demultiplexer) diff --git a/comp/README.md b/comp/README.md index 9f16e8fe6416e8..0f72975116e4f8 100644 --- a/comp/README.md +++ b/comp/README.md @@ -250,47 +250,6 @@ supported Datadog intakes. -## [comp/snmptraps](https://pkg.go.dev/github.com/DataDog/dd-agent-comp-experiments/comp/snmptraps) (Component Bundle) - -*Datadog Team*: network-device-monitoring - -Package snmptraps implements the a server that listens for SNMP trap data -and sends it to the backend. - -### [comp/snmptraps/config](https://pkg.go.dev/github.com/DataDog/dd-agent-comp-experiments/comp/snmptraps/config) - -Package config implements the configuration type for the traps server and -a component that provides it. - -### [comp/snmptraps/formatter](https://pkg.go.dev/github.com/DataDog/dd-agent-comp-experiments/comp/snmptraps/formatter) - -Package formatter provides a component for formatting SNMP traps. - -### [comp/snmptraps/forwarder](https://pkg.go.dev/github.com/DataDog/dd-agent-comp-experiments/comp/snmptraps/forwarder) - -Package forwarder defines a component that receives trap data from the -listener component, formats it properly, and sends it to the backend. - -### [comp/snmptraps/listener](https://pkg.go.dev/github.com/DataDog/dd-agent-comp-experiments/comp/snmptraps/listener) - -Package listener implements a component that listens for SNMP messages, -parses them, and publishes messages on a channel. - -### [comp/snmptraps/oidresolver](https://pkg.go.dev/github.com/DataDog/dd-agent-comp-experiments/comp/snmptraps/oidresolver) - -Package oidresolver resolves OIDs - -### [comp/snmptraps/server](https://pkg.go.dev/github.com/DataDog/dd-agent-comp-experiments/comp/snmptraps/server) - -Package server implements a component that runs the traps server. -It listens for SNMP trap messages on a configured port, parses and -reformats them, and sends the resulting data to the backend. - -### [comp/snmptraps/status](https://pkg.go.dev/github.com/DataDog/dd-agent-comp-experiments/comp/snmptraps/status) - -Package status exposes the expvars we use for status tracking to the -component system. - ## [comp/systray](https://pkg.go.dev/github.com/DataDog/dd-agent-comp-experiments/comp/systray) (Component Bundle) *Datadog Team*: windows-agent diff --git a/comp/snmptraps/bundle.go b/comp/snmptraps/bundle.go deleted file mode 100644 index f6b25464039b13..00000000000000 --- a/comp/snmptraps/bundle.go +++ /dev/null @@ -1,32 +0,0 @@ -// 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 2023-present Datadog, Inc. - -// Package snmptraps implements the a server that listens for SNMP trap data -// and sends it to the backend. -package snmptraps - -import ( - "github.com/DataDog/datadog-agent/comp/snmptraps/config/configimpl" - "github.com/DataDog/datadog-agent/comp/snmptraps/formatter/formatterimpl" - "github.com/DataDog/datadog-agent/comp/snmptraps/forwarder/forwarderimpl" - "github.com/DataDog/datadog-agent/comp/snmptraps/listener/listenerimpl" - "github.com/DataDog/datadog-agent/comp/snmptraps/oidresolver/oidresolverimpl" - "github.com/DataDog/datadog-agent/comp/snmptraps/server/serverimpl" - "github.com/DataDog/datadog-agent/comp/snmptraps/status/statusimpl" - "github.com/DataDog/datadog-agent/pkg/util/fxutil" -) - -// team: network-device-monitoring - -// Bundle defines the fx options for this bundle. -var Bundle = fxutil.Bundle( - configimpl.Module, - formatterimpl.Module, - forwarderimpl.Module, - listenerimpl.Module, - oidresolverimpl.Module, - statusimpl.Module, - serverimpl.Module, -) diff --git a/comp/snmptraps/bundle_test.go b/comp/snmptraps/bundle_test.go deleted file mode 100644 index 86e56d124a7294..00000000000000 --- a/comp/snmptraps/bundle_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// 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 2023-present Datadog, Inc. - -package snmptraps - -import ( - "testing" - - "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexer" - "github.com/DataDog/datadog-agent/comp/core/config" - "github.com/DataDog/datadog-agent/comp/core/hostname/hostnameimpl" - "github.com/DataDog/datadog-agent/comp/core/log" - "github.com/DataDog/datadog-agent/comp/forwarder/defaultforwarder" - "github.com/DataDog/datadog-agent/pkg/util/fxutil" -) - -func TestBundleDependencies(t *testing.T) { - fxutil.TestBundle(t, Bundle, - config.MockModule, - hostnameimpl.MockModule, - log.MockModule, - demultiplexer.MockModule, - defaultforwarder.MockModule, - ) -} diff --git a/comp/snmptraps/config/component.go b/comp/snmptraps/config/component.go deleted file mode 100644 index 1fa60dee43b7ef..00000000000000 --- a/comp/snmptraps/config/component.go +++ /dev/null @@ -1,15 +0,0 @@ -// 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 2023-present Datadog, Inc. - -// Package config implements the configuration type for the traps server and -// a component that provides it. -package config - -// team: network-device-monitoring - -// Component is the component type. -type Component interface { - Get() *TrapsConfig -} diff --git a/comp/snmptraps/config/config_test.go b/comp/snmptraps/config/config_test.go deleted file mode 100644 index 96e1c90ca05582..00000000000000 --- a/comp/snmptraps/config/config_test.go +++ /dev/null @@ -1,234 +0,0 @@ -// 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 2020-present Datadog, Inc. - -package config - -import ( - "context" - "strings" - "testing" - - "github.com/DataDog/datadog-agent/comp/core/config" - "github.com/DataDog/datadog-agent/comp/core/hostname" - "github.com/DataDog/datadog-agent/comp/core/hostname/hostnameimpl" - "github.com/DataDog/datadog-agent/comp/core/log" - "github.com/DataDog/datadog-agent/pkg/util/fxutil" - "github.com/gosnmp/gosnmp" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/fx" - "gopkg.in/yaml.v2" -) - -const mockedHostname = "VeryLongHostnameThatDoesNotFitIntoTheByteArray" - -var expectedEngineID = "\x80\xff\xff\xff\xff\x67\xb2\x0f\xe4\xdf\x73\x7a\xce\x28\x47\x03\x8f\x57\xe6\x5c\x98" - -var expectedEngineIDs = map[string]string{ - "VeryLongHostnameThatDoesNotFitIntoTheByteArray": "\x80\xff\xff\xff\xff\x67\xb2\x0f\xe4\xdf\x73\x7a\xce\x28\x47\x03\x8f\x57\xe6\x5c\x98", - "VeryLongHostnameThatIsDifferent": "\x80\xff\xff\xff\xff\xe7\x21\xcc\xd7\x0b\xe1\x60\xc5\x18\xd7\xde\x17\x86\xb0\x7d\x36", -} - -// structify converts any yamlizable object to a plain map[string]any -func structify[T any](obj T) (map[string]any, error) { - out, err := yaml.Marshal(obj) - if err != nil { - return nil, err - } - result := make(map[string]any) - err = yaml.Unmarshal(out, result) - if err != nil { - return nil, err - } - return result, nil -} - -// withConfig returns an fx Module providing the default datadog config -// overridden with the given network device namespace and traps configuration. -// In tests for other things, prefer to just inject the trapConfig rather than -// building and parsing an entire fake DD config. -func withConfig(t testing.TB, trapConfig *TrapsConfig, globalNamespace string) fx.Option { - t.Helper() - overrides := make(map[string]interface{}) - if globalNamespace != "" { - overrides["network_devices.namespace"] = globalNamespace - } - if trapConfig != nil { - rawTrapConfig, err := structify(trapConfig) - require.NoError(t, err) - overrides["network_devices.snmp_traps"] = rawTrapConfig - } - return fx.Options( - config.MockModule, - fx.Replace(config.MockParams{Overrides: overrides}), - ) -} - -func buildConfig(conf config.Component, hnService hostname.Component) (*TrapsConfig, error) { - name, err := hnService.Get(context.Background()) - if err != nil { - return nil, err - } - c, err := ReadConfig(name, conf) - if err != nil { - return nil, err - } - return c, nil -} - -// testOptions provides several fx options that multiple tests need -var testOptions = fx.Options( - fx.Provide(buildConfig), - hostnameimpl.MockModule, - fx.Replace(hostnameimpl.MockHostname(mockedHostname)), - log.MockModule, -) - -func TestFullConfig(t *testing.T) { - deps := fxutil.Test[struct { - fx.In - Config *TrapsConfig - Logger log.Component - }](t, - testOptions, - withConfig(t, &TrapsConfig{ - Port: 1234, - Users: []UserV3{ - { - Username: "user", - AuthKey: "password", - AuthProtocol: "MD5", - PrivKey: "password", - PrivProtocol: "AES", - }, - }, - BindHost: "127.0.0.1", - CommunityStrings: []string{"public"}, - StopTimeout: 12, - Namespace: "foo", - }, ""), - ) - config := deps.Config - logger := deps.Logger - assert.Equal(t, uint16(1234), config.Port) - assert.Equal(t, 12, config.StopTimeout) - assert.Equal(t, []string{"public"}, config.CommunityStrings) - assert.Equal(t, "127.0.0.1", config.BindHost) - assert.Equal(t, "foo", config.Namespace) - assert.Equal(t, []UserV3{ - { - Username: "user", - AuthKey: "password", - AuthProtocol: "MD5", - PrivKey: "password", - PrivProtocol: "AES", - }, - }, config.Users) - - params, err := config.BuildSNMPParams(logger) - assert.NoError(t, err) - assert.Equal(t, uint16(1234), params.Port) - assert.Equal(t, gosnmp.Version3, params.Version) - assert.Equal(t, "udp", params.Transport) - assert.NotNil(t, params.Logger) - assert.Equal(t, gosnmp.UserSecurityModel, params.SecurityModel) - assert.Equal(t, &gosnmp.UsmSecurityParameters{ - UserName: "user", - AuthoritativeEngineID: expectedEngineID, - AuthenticationProtocol: gosnmp.MD5, - AuthenticationPassphrase: "password", - PrivacyProtocol: gosnmp.AES, - PrivacyPassphrase: "password", - }, params.SecurityParameters) -} - -func TestMinimalConfig(t *testing.T) { - deps := fxutil.Test[struct { - fx.In - Config *TrapsConfig - Logger log.Component - }](t, - config.MockModule, - testOptions, - ) - config := deps.Config - logger := deps.Logger - assert.Equal(t, uint16(9162), config.Port) - assert.Equal(t, 5, config.StopTimeout) - assert.Empty(t, config.CommunityStrings) - assert.Equal(t, "0.0.0.0", config.BindHost) - assert.Empty(t, config.Users) - assert.Equal(t, "default", config.Namespace) - - params, err := config.BuildSNMPParams(logger) - assert.NoError(t, err) - assert.Equal(t, uint16(9162), params.Port) - assert.Equal(t, gosnmp.Version2c, params.Version) - assert.Equal(t, "udp", params.Transport) - assert.NotNil(t, params.Logger) - assert.Equal(t, nil, params.SecurityParameters) -} - -func TestDefaultUsers(t *testing.T) { - config := fxutil.Test[*TrapsConfig](t, - testOptions, - withConfig(t, &TrapsConfig{ - CommunityStrings: []string{"public"}, - StopTimeout: 11, - }, ""), - ) - assert.Equal(t, 11, config.StopTimeout) -} - -func TestBuildAuthoritativeEngineID(t *testing.T) { - for name, engineID := range expectedEngineIDs { - config := fxutil.Test[*TrapsConfig](t, - config.MockModule, - fx.Provide(buildConfig), - hostnameimpl.MockModule, - fx.Replace(hostnameimpl.MockHostname(name)), - ) - assert.Equal(t, engineID, config.authoritativeEngineID) - } -} - -func TestNamespaceIsNormalized(t *testing.T) { - config := fxutil.Test[*TrapsConfig](t, - testOptions, - withConfig(t, &TrapsConfig{ - Namespace: "><\n\r\tfoo", - }, ""), - ) - assert.Equal(t, "--foo", config.Namespace) -} - -func TestInvalidNamespace(t *testing.T) { - ddConfig := fxutil.Test[config.Component](t, - withConfig(t, &TrapsConfig{ - Namespace: strings.Repeat("x", 101), - }, "")) - _, err := ReadConfig("", ddConfig) - require.Error(t, err) - assert.Contains(t, err.Error(), "too long") -} - -func TestNamespaceSetGlobally(t *testing.T) { - config := fxutil.Test[*TrapsConfig](t, - testOptions, - withConfig(t, nil, "foo"), - ) - assert.Equal(t, "foo", config.Namespace) -} - -func TestNamespaceSetBothGloballyAndLocally(t *testing.T) { - config := fxutil.Test[*TrapsConfig](t, - testOptions, - withConfig(t, - &TrapsConfig{Namespace: "bar"}, - "foo"), - ) - - assert.Equal(t, "bar", config.Namespace) -} diff --git a/comp/snmptraps/config/configimpl/service.go b/comp/snmptraps/config/configimpl/service.go deleted file mode 100644 index 7c676e21a79c72..00000000000000 --- a/comp/snmptraps/config/configimpl/service.go +++ /dev/null @@ -1,43 +0,0 @@ -// 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 2023-present Datadog, Inc. - -// Package configimpl implements the config service. -package configimpl - -import ( - "context" - - "github.com/DataDog/datadog-agent/comp/core/config" - "github.com/DataDog/datadog-agent/comp/core/hostname" - trapsconf "github.com/DataDog/datadog-agent/comp/snmptraps/config" - "github.com/DataDog/datadog-agent/pkg/util/fxutil" - "go.uber.org/fx" -) - -type configService struct { - conf *trapsconf.TrapsConfig -} - -// Get returns the configuration. -func (cs *configService) Get() *trapsconf.TrapsConfig { - return cs.conf -} - -func newService(conf config.Component, hnService hostname.Component) (trapsconf.Component, error) { - name, err := hnService.Get(context.Background()) - if err != nil { - return nil, err - } - c, err := trapsconf.ReadConfig(name, conf) - if err != nil { - return nil, err - } - return &configService{c}, nil -} - -// Module defines the fx options for this component. -var Module = fxutil.Component( - fx.Provide(newService), -) diff --git a/comp/snmptraps/config/configimpl/service_mock.go b/comp/snmptraps/config/configimpl/service_mock.go deleted file mode 100644 index a82b2cdc649b83..00000000000000 --- a/comp/snmptraps/config/configimpl/service_mock.go +++ /dev/null @@ -1,45 +0,0 @@ -// 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 2023-present Datadog, Inc. - -package configimpl - -import ( - "context" - - "github.com/DataDog/datadog-agent/comp/core/hostname" - trapsconf "github.com/DataDog/datadog-agent/comp/snmptraps/config" - "github.com/DataDog/datadog-agent/pkg/util/fxutil" - "go.uber.org/fx" -) - -type dependencies struct { - fx.In - HostnameService hostname.Component `optional:"true"` - Conf *trapsconf.TrapsConfig -} - -func newMockConfig(dep dependencies) (trapsconf.Component, error) { - host := "my-hostname" - if dep.HostnameService != nil { - var err error - host, err = dep.HostnameService.Get(context.Background()) - if err != nil { - return nil, err - } - } - tc := dep.Conf - if err := tc.SetDefaults(host, "default"); err != nil { - return nil, err - } - return &configService{conf: tc}, nil -} - -// MockModule provides the default config, and allows tests to override it by -// providing `fx.Replace(&TrapsConfig{...})`; a value replaced this way will -// have default values set sensibly if they aren't provided. -var MockModule = fxutil.Component( - fx.Provide(newMockConfig), - fx.Supply(&trapsconf.TrapsConfig{Enabled: true}), -) diff --git a/comp/snmptraps/formatter/component.go b/comp/snmptraps/formatter/component.go deleted file mode 100644 index cf792e0ad566f2..00000000000000 --- a/comp/snmptraps/formatter/component.go +++ /dev/null @@ -1,18 +0,0 @@ -// 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 2023-present Datadog, Inc. - -// Package formatter provides a component for formatting SNMP traps. -package formatter - -import ( - "github.com/DataDog/datadog-agent/comp/snmptraps/packet" -) - -// team: network-device-monitoring - -// Component is the component type. -type Component interface { - FormatPacket(packet *packet.SnmpPacket) ([]byte, error) -} diff --git a/comp/snmptraps/formatter/formatterimpl/mock_test.go b/comp/snmptraps/formatter/formatterimpl/mock_test.go deleted file mode 100644 index b8796d0cfafbe9..00000000000000 --- a/comp/snmptraps/formatter/formatterimpl/mock_test.go +++ /dev/null @@ -1,25 +0,0 @@ -// 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 2020-present Datadog, Inc. - -package formatterimpl - -import ( - "testing" - - "github.com/DataDog/datadog-agent/comp/snmptraps/formatter" - "github.com/DataDog/datadog-agent/comp/snmptraps/packet" - "github.com/DataDog/datadog-agent/pkg/util/fxutil" - - "github.com/stretchr/testify/require" -) - -func TestMockFormatter(t *testing.T) { - formatter := fxutil.Test[formatter.Component](t, MockModule) - packet := packet.CreateTestV1GenericPacket() - // we don't check the value itself because it uses "encoding/gob", which - // produces different values depending on the platform. - _, err := formatter.FormatPacket(packet) - require.NoError(t, err) -} diff --git a/comp/snmptraps/forwarder/component.go b/comp/snmptraps/forwarder/component.go deleted file mode 100644 index f8675a8c5d3b48..00000000000000 --- a/comp/snmptraps/forwarder/component.go +++ /dev/null @@ -1,13 +0,0 @@ -// 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 2023-present Datadog, Inc. - -// Package forwarder defines a component that receives trap data from the -// listener component, formats it properly, and sends it to the backend. -package forwarder - -// team: network-device-monitoring - -// Component is the component type. -type Component interface{} diff --git a/comp/snmptraps/forwarder/forwarderimpl/forwarder.go b/comp/snmptraps/forwarder/forwarderimpl/forwarder.go deleted file mode 100644 index 590688c1f5ba17..00000000000000 --- a/comp/snmptraps/forwarder/forwarderimpl/forwarder.go +++ /dev/null @@ -1,121 +0,0 @@ -// 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 2022-present Datadog, Inc. - -// Package forwarderimpl implements the forwarder component. -package forwarderimpl - -import ( - "context" - "time" - - "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexer" - "github.com/DataDog/datadog-agent/comp/core/log" - "github.com/DataDog/datadog-agent/comp/snmptraps/config" - "github.com/DataDog/datadog-agent/comp/snmptraps/formatter" - "github.com/DataDog/datadog-agent/comp/snmptraps/forwarder" - "github.com/DataDog/datadog-agent/comp/snmptraps/listener" - "github.com/DataDog/datadog-agent/comp/snmptraps/packet" - "github.com/DataDog/datadog-agent/pkg/aggregator/sender" - "github.com/DataDog/datadog-agent/pkg/epforwarder" - "github.com/DataDog/datadog-agent/pkg/util/fxutil" - "go.uber.org/fx" -) - -// Module defines the fx options for this component. -var Module = fxutil.Component( - fx.Provide(newTrapForwarder), -) - -// trapForwarder consumes SNMP packets, formats traps and send them as EventPlatformEvents -// The trapForwarder is an intermediate step between the listener and the epforwarder in order to limit the processing of the listener -// to the minimum. The forwarder process payloads received by the listener via the trapsIn channel, formats them and finally -// give them to the epforwarder for sending it to Datadog. -type trapForwarder struct { - trapsIn packet.PacketsChannel - formatter formatter.Component - sender sender.Sender - stopChan chan struct{} - logger log.Component -} - -type dependencies struct { - fx.In - Config config.Component - Formatter formatter.Component - Demux demultiplexer.Component - Listener listener.Component - Logger log.Component -} - -// newTrapForwarder creates a simple TrapForwarder instance -func newTrapForwarder(lc fx.Lifecycle, dep dependencies) (forwarder.Component, error) { - sender, err := dep.Demux.GetDefaultSender() - if err != nil { - return nil, err - } - tf := &trapForwarder{ - trapsIn: dep.Listener.Packets(), - formatter: dep.Formatter, - sender: sender, - stopChan: make(chan struct{}, 1), - logger: dep.Logger, - } - conf := dep.Config.Get() - if conf.Enabled { - lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - tf.Start() - return nil - }, - OnStop: func(ctx context.Context) error { - tf.Stop() - return nil - }, - }) - } - - return tf, nil -} - -// Start the TrapForwarder instance. Need to Stop it manually. -func (tf *trapForwarder) Start() { - tf.logger.Info("Starting TrapForwarder") - go tf.run() -} - -// Stop the TrapForwarder instance. -func (tf *trapForwarder) Stop() { - select { - case tf.stopChan <- struct{}{}: - default: - tf.logger.Warn("TrapForwarder stopped twice.") - } -} - -func (tf *trapForwarder) run() { - flushTicker := time.NewTicker(10 * time.Second).C - for { - select { - case <-tf.stopChan: - tf.logger.Info("Stopped TrapForwarder") - return - case packet := <-tf.trapsIn: - tf.sendTrap(packet) - case <-flushTicker: - tf.sender.Commit() // Commit metrics - } - } -} - -func (tf *trapForwarder) sendTrap(packet *packet.SnmpPacket) { - data, err := tf.formatter.FormatPacket(packet) - if err != nil { - tf.logger.Errorf("failed to format packet: %s", err) - return - } - tf.logger.Tracef("send trap payload: %s", string(data)) - tf.sender.Count("datadog.snmp_traps.forwarded", 1, "", packet.GetTags()) - tf.sender.EventPlatformEvent(data, epforwarder.EventTypeSnmpTraps) -} diff --git a/comp/snmptraps/forwarder/forwarderimpl/forwarder_test.go b/comp/snmptraps/forwarder/forwarderimpl/forwarder_test.go deleted file mode 100644 index 8fc5d1409b8daa..00000000000000 --- a/comp/snmptraps/forwarder/forwarderimpl/forwarder_test.go +++ /dev/null @@ -1,102 +0,0 @@ -// 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 2022-present Datadog, Inc. - -package forwarderimpl - -import ( - "net" - "testing" - "time" - - "github.com/gosnmp/gosnmp" - "github.com/stretchr/testify/require" - "go.uber.org/fx" - - "github.com/DataDog/datadog-agent/comp/core/log" - "github.com/DataDog/datadog-agent/comp/snmptraps/config/configimpl" - "github.com/DataDog/datadog-agent/comp/snmptraps/formatter" - "github.com/DataDog/datadog-agent/comp/snmptraps/formatter/formatterimpl" - "github.com/DataDog/datadog-agent/comp/snmptraps/forwarder" - "github.com/DataDog/datadog-agent/comp/snmptraps/listener" - "github.com/DataDog/datadog-agent/comp/snmptraps/listener/listenerimpl" - "github.com/DataDog/datadog-agent/comp/snmptraps/packet" - "github.com/DataDog/datadog-agent/comp/snmptraps/senderhelper" - "github.com/DataDog/datadog-agent/pkg/aggregator/mocksender" - "github.com/DataDog/datadog-agent/pkg/epforwarder" - "github.com/DataDog/datadog-agent/pkg/util/fxutil" -) - -var simpleUDPAddr = &net.UDPAddr{IP: net.IPv4(1, 1, 1, 1), Port: 161} - -type services struct { - fx.In - Sender *mocksender.MockSender - Formatter formatter.Component - Listener listener.MockComponent - Forwarder forwarder.Component -} - -func setUp(t *testing.T) *services { - t.Helper() - s := fxutil.Test[services](t, - configimpl.MockModule, - log.MockModule, - senderhelper.Opts, - formatterimpl.MockModule, - listenerimpl.MockModule, - Module, - ) - return &s -} - -func makeSnmpPacket(trap gosnmp.SnmpTrap) *packet.SnmpPacket { - gosnmpPacket := &gosnmp.SnmpPacket{ - Version: gosnmp.Version2c, - Community: "public", - Variables: trap.Variables, - SnmpTrap: trap, - } - return &packet.SnmpPacket{ - Content: gosnmpPacket, - Addr: simpleUDPAddr, - Namespace: "totoro", - Timestamp: time.Now().UnixMilli()} -} - -func TestV1GenericTrapAreForwarder(t *testing.T) { - s := setUp(t) - packet := makeSnmpPacket(packet.LinkDownv1GenericTrap) - rawEvent, err := s.Formatter.FormatPacket(packet) - require.NoError(t, err) - s.Listener.Send(packet) - time.Sleep(100 * time.Millisecond) - s.Sender.AssertEventPlatformEvent(t, rawEvent, epforwarder.EventTypeSnmpTraps) -} - -func TestV1SpecificTrapAreForwarder(t *testing.T) { - s := setUp(t) - packet := makeSnmpPacket(packet.AlarmActiveStatev1SpecificTrap) - rawEvent, err := s.Formatter.FormatPacket(packet) - require.NoError(t, err) - s.Listener.Send(packet) - time.Sleep(100 * time.Millisecond) - s.Sender.AssertEventPlatformEvent(t, rawEvent, epforwarder.EventTypeSnmpTraps) -} -func TestV2TrapAreForwarder(t *testing.T) { - s := setUp(t) - packet := makeSnmpPacket(packet.NetSNMPExampleHeartbeatNotification) - rawEvent, err := s.Formatter.FormatPacket(packet) - require.NoError(t, err) - s.Listener.Send(packet) - time.Sleep(100 * time.Millisecond) - s.Sender.AssertEventPlatformEvent(t, rawEvent, epforwarder.EventTypeSnmpTraps) -} - -func TestForwarderTelemetry(t *testing.T) { - s := setUp(t) - s.Listener.Send(makeSnmpPacket(packet.NetSNMPExampleHeartbeatNotification)) - time.Sleep(100 * time.Millisecond) - s.Sender.AssertMetric(t, "Count", "datadog.snmp_traps.forwarded", 1, "", []string{"snmp_device:1.1.1.1", "device_namespace:totoro", "snmp_version:2"}) -} diff --git a/comp/snmptraps/listener/component.go b/comp/snmptraps/listener/component.go deleted file mode 100644 index e97b4fe25b4736..00000000000000 --- a/comp/snmptraps/listener/component.go +++ /dev/null @@ -1,20 +0,0 @@ -// 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 2023-present Datadog, Inc. - -// Package listener implements a component that listens for SNMP messages, -// parses them, and publishes messages on a channel. -package listener - -import ( - "github.com/DataDog/datadog-agent/comp/snmptraps/packet" -) - -// team: network-device-monitoring - -// Component is the component type. -type Component interface { - // Packets returns the channel to which the listener publishes traps packets. - Packets() packet.PacketsChannel -} diff --git a/comp/snmptraps/listener/listenerimpl/mock_listener.go b/comp/snmptraps/listener/listenerimpl/mock_listener.go deleted file mode 100644 index b54d87067fdf6a..00000000000000 --- a/comp/snmptraps/listener/listenerimpl/mock_listener.go +++ /dev/null @@ -1,46 +0,0 @@ -// 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 2022-present Datadog, Inc. - -package listenerimpl - -import ( - "github.com/DataDog/datadog-agent/comp/snmptraps/listener" - "github.com/DataDog/datadog-agent/comp/snmptraps/packet" - "github.com/DataDog/datadog-agent/pkg/util/fxutil" - "go.uber.org/fx" -) - -// MockModule provides a MockComponent as the Component. -var MockModule = fxutil.Component( - fx.Provide(newMock), -) - -type mockListener struct { - packets packet.PacketsChannel -} - -func newMock() (listener.MockComponent, listener.Component) { - l := &mockListener{ - packets: make(chan *packet.SnmpPacket, 100), - } - return l, l -} - -// Packets returns the packets channel to which the listener publishes. -func (t *mockListener) Packets() packet.PacketsChannel { - return t.packets -} - -// Start is a no-op -func (t *mockListener) Start() error { - return nil -} - -// Stop is a no-op -func (t *mockListener) Stop() {} - -func (t *mockListener) Send(p *packet.SnmpPacket) { - t.packets <- p -} diff --git a/comp/snmptraps/listener/mock.go b/comp/snmptraps/listener/mock.go deleted file mode 100644 index 540a1d6162d395..00000000000000 --- a/comp/snmptraps/listener/mock.go +++ /dev/null @@ -1,16 +0,0 @@ -// 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 2022-present Datadog, Inc. - -package listener - -import ( - "github.com/DataDog/datadog-agent/comp/snmptraps/packet" -) - -// MockComponent just holds a channel to which tests can write. -type MockComponent interface { - Component - Send(*packet.SnmpPacket) -} diff --git a/comp/snmptraps/oidresolver/component.go b/comp/snmptraps/oidresolver/component.go deleted file mode 100644 index 7ed352f8ed8bee..00000000000000 --- a/comp/snmptraps/oidresolver/component.go +++ /dev/null @@ -1,15 +0,0 @@ -// 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 2023-present Datadog, Inc. - -// Package oidresolver resolves OIDs -package oidresolver - -// team: network-device-monitoring - -// Component is a interface to get Trap and Variable metadata from OIDs -type Component interface { - GetTrapMetadata(trapOID string) (TrapMetadata, error) - GetVariableMetadata(trapOID string, varOID string) (VariableMetadata, error) -} diff --git a/comp/snmptraps/oidresolver/oidresolverimpl/mock.go b/comp/snmptraps/oidresolver/oidresolverimpl/mock.go deleted file mode 100644 index f0f77d19622f9a..00000000000000 --- a/comp/snmptraps/oidresolver/oidresolverimpl/mock.go +++ /dev/null @@ -1,111 +0,0 @@ -// 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 2022-present Datadog, Inc. - -package oidresolverimpl - -import ( - "fmt" - - "github.com/DataDog/datadog-agent/comp/snmptraps/oidresolver" - "github.com/DataDog/datadog-agent/pkg/util/fxutil" - "go.uber.org/fx" -) - -// MockModule provides a dummy resolver with canned data. -// Set your own data with fx.Replace(&oidresolver.TrapDBFileContent{...}) -var MockModule = fxutil.Component( - fx.Provide(NewMockResolver), - fx.Supply(&dummyTrapDB), -) - -// MockResolver implements OIDResolver with a mock database. -type MockResolver struct { - content *oidresolver.TrapDBFileContent -} - -// GetTrapMetadata implements OIDResolver#GetTrapMetadata. -func (r MockResolver) GetTrapMetadata(trapOid string) (oidresolver.TrapMetadata, error) { - trapOid = oidresolver.NormalizeOID(trapOid) - trapData, ok := r.content.Traps[trapOid] - if !ok { - return oidresolver.TrapMetadata{}, fmt.Errorf("trap OID %s is not defined", trapOid) - } - return trapData, nil -} - -// GetVariableMetadata implements OIDResolver#GetVariableMetadata. -func (r MockResolver) GetVariableMetadata(string, varOid string) (oidresolver.VariableMetadata, error) { //nolint:revive // TODO fix revive unusued-parameter - varOid = oidresolver.NormalizeOID(varOid) - varData, ok := r.content.Variables[varOid] - if !ok { - return oidresolver.VariableMetadata{}, fmt.Errorf("variable OID %s is not defined", varOid) - } - return varData, nil -} - -// NewMockResolver creates a mock resolver populated with fake data. -func NewMockResolver(content *oidresolver.TrapDBFileContent) oidresolver.Component { - return &MockResolver{content} -} - -var dummyTrapDB = oidresolver.TrapDBFileContent{ - Traps: oidresolver.TrapSpec{ - "1.3.6.1.6.3.1.1.5.3": oidresolver.TrapMetadata{Name: "ifDown", MIBName: "IF-MIB"}, // v1 Trap - "1.3.6.1.4.1.8072.2.3.0.1": oidresolver.TrapMetadata{Name: "netSnmpExampleHeartbeatNotification", MIBName: "NET-SNMP-EXAMPLES-MIB"}, // v2+ - "1.3.6.1.6.3.1.1.5.4": oidresolver.TrapMetadata{Name: "linkUp", MIBName: "IF-MIB"}, - }, - Variables: oidresolver.VariableSpec{ - "1.3.6.1.2.1.2.2.1.1": oidresolver.VariableMetadata{Name: "ifIndex"}, - "1.3.6.1.2.1.2.2.1.7": oidresolver.VariableMetadata{Name: "ifAdminStatus", Enumeration: map[int]string{1: "up", 2: "down", 3: "testing"}}, - "1.3.6.1.2.1.2.2.1.8": oidresolver.VariableMetadata{Name: "ifOperStatus", Enumeration: map[int]string{1: "up", 2: "down", 3: "testing", 4: "unknown", 5: "dormant", 6: "notPresent", 7: "lowerLayerDown"}}, - "1.3.6.1.4.1.8072.2.3.2.1": oidresolver.VariableMetadata{Name: "netSnmpExampleHeartbeatRate"}, - "1.3.6.1.2.1.200.1.1.1.3": oidresolver.VariableMetadata{Name: "pwCepSonetConfigErrorOrStatus", Bits: map[int]string{ - 0: "other", - 1: "timeslotInUse", - 2: "timeslotMisuse", - 3: "peerDbaIncompatible", - 4: "peerEbmIncompatible", - 5: "peerRtpIncompatible", - 6: "peerAsyncIncompatible", - 7: "peerDbaAsymmetric", - 8: "peerEbmAsymmetric", - 9: "peerRtpAsymmetric", - 10: "peerAsyncAsymmetric", - }}, - "1.3.6.1.2.1.200.1.3.1.5": oidresolver.VariableMetadata{Name: "myFakeVarType", Bits: map[int]string{ - 0: "test0", - 1: "test1", - 3: "test3", - 5: "test5", - 6: "test6", - 12: "test12", - 15: "test15", - 130: "test130", - }}, - "1.3.6.1.2.1.200.1.3.1.6": oidresolver.VariableMetadata{ - Name: "myBadVarType", - Enumeration: map[int]string{ - 0: "test0", - 1: "test1", - 3: "test3", - 5: "test5", - 6: "test6", - 12: "test12", - 15: "test15", - 130: "test130", - }, - Bits: map[int]string{ - 0: "test0", - 1: "test1", - 3: "test3", - 5: "test5", - 6: "test6", - 12: "test12", - 15: "test15", - 130: "test130", - }, - }, - }, -} diff --git a/comp/snmptraps/senderhelper/senderhelper.go b/comp/snmptraps/senderhelper/senderhelper.go deleted file mode 100644 index 3ca92140cac206..00000000000000 --- a/comp/snmptraps/senderhelper/senderhelper.go +++ /dev/null @@ -1,37 +0,0 @@ -// 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 2022-present Datadog, Inc. - -//go:build test - -// Package senderhelper provides a set of fx options for providing a mock -// sender for the demultiplexer. -package senderhelper - -import ( - "go.uber.org/fx" - - "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexer" - "github.com/DataDog/datadog-agent/comp/core/config" - "github.com/DataDog/datadog-agent/comp/forwarder/defaultforwarder" - "github.com/DataDog/datadog-agent/pkg/aggregator/mocksender" - "github.com/DataDog/datadog-agent/pkg/aggregator/sender" -) - -// Opts is a set of options for providing a demux with a mock sender. -// We can remove this if the Sender is ever exposed as a component. -var Opts = fx.Options( - defaultforwarder.MockModule, - demultiplexer.MockModule, - config.MockModule, - fx.Provide(func() (*mocksender.MockSender, sender.Sender) { - mockSender := mocksender.NewMockSender("mock-sender") - mockSender.SetupAcceptAll() - return mockSender, mockSender - }), - fx.Decorate(func(demux demultiplexer.Mock, s sender.Sender) demultiplexer.Component { - demux.SetDefaultSender(s) - return demux - }), -) diff --git a/comp/snmptraps/server/component.go b/comp/snmptraps/server/component.go deleted file mode 100644 index 2b09d8e0897295..00000000000000 --- a/comp/snmptraps/server/component.go +++ /dev/null @@ -1,14 +0,0 @@ -// 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 2023-present Datadog, Inc. - -// Package server implements a component that runs the traps server. -// It listens for SNMP trap messages on a configured port, parses and -// reformats them, and sends the resulting data to the backend. -package server - -// team: network-device-monitoring - -// Component is the component type. -type Component interface{} diff --git a/comp/snmptraps/server/serverimpl/server.go b/comp/snmptraps/server/serverimpl/server.go deleted file mode 100644 index 3a90a6b664f633..00000000000000 --- a/comp/snmptraps/server/serverimpl/server.go +++ /dev/null @@ -1,38 +0,0 @@ -// 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 2020-present Datadog, Inc. - -// Package serverimpl implements the traps server. -package serverimpl - -import ( - "github.com/DataDog/datadog-agent/comp/snmptraps/forwarder" - "github.com/DataDog/datadog-agent/comp/snmptraps/listener" - "github.com/DataDog/datadog-agent/comp/snmptraps/server" - "github.com/DataDog/datadog-agent/pkg/util/fxutil" - "go.uber.org/fx" -) - -// Module defines the fx options for this component. -var Module = fxutil.Component( - fx.Provide(newServer), -) - -// Server manages an SNMP trap listener. -type Server struct { - listener listener.Component - forwarder forwarder.Component -} - -// newServer configures a netflow server. -// Since the listener and forwarder are already registered with the lifecycle -// tracker, this component really just exists so that there's one component to -// register instead of requiring users to remember to depend on both the -// listener and the forwarder. -func newServer(l listener.Component, f forwarder.Component) server.Component { - return &Server{ - listener: l, - forwarder: f, - } -} diff --git a/comp/snmptraps/server/serverimpl/server_test.go b/comp/snmptraps/server/serverimpl/server_test.go deleted file mode 100644 index 50af7f6919eeef..00000000000000 --- a/comp/snmptraps/server/serverimpl/server_test.go +++ /dev/null @@ -1,51 +0,0 @@ -// 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 2020-present Datadog, Inc. - -//go:build test - -package serverimpl - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/fx" - - "github.com/DataDog/datadog-agent/comp/core/hostname/hostnameimpl" - "github.com/DataDog/datadog-agent/comp/core/log" - "github.com/DataDog/datadog-agent/comp/snmptraps/config" - "github.com/DataDog/datadog-agent/comp/snmptraps/config/configimpl" - "github.com/DataDog/datadog-agent/comp/snmptraps/formatter/formatterimpl" - "github.com/DataDog/datadog-agent/comp/snmptraps/forwarder/forwarderimpl" - "github.com/DataDog/datadog-agent/comp/snmptraps/listener/listenerimpl" - "github.com/DataDog/datadog-agent/comp/snmptraps/senderhelper" - "github.com/DataDog/datadog-agent/comp/snmptraps/server" - "github.com/DataDog/datadog-agent/comp/snmptraps/status/statusimpl" - ndmtestutils "github.com/DataDog/datadog-agent/pkg/networkdevice/testutils" - "github.com/DataDog/datadog-agent/pkg/util/fxutil" -) - -func TestStartStop(t *testing.T) { - freePort, err := ndmtestutils.GetFreePort() - require.NoError(t, err) - server := fxutil.Test[server.Component](t, - log.MockModule, - configimpl.MockModule, - formatterimpl.MockModule, - senderhelper.Opts, - hostnameimpl.MockModule, - forwarderimpl.Module, - statusimpl.MockModule, - listenerimpl.Module, - Module, - fx.Replace(&config.TrapsConfig{ - Enabled: true, - Port: freePort, - CommunityStrings: []string{"public"}, - }), - ) - assert.NotEmpty(t, server) -} diff --git a/comp/snmptraps/status/component.go b/comp/snmptraps/status/component.go deleted file mode 100644 index fac51cfa691011..00000000000000 --- a/comp/snmptraps/status/component.go +++ /dev/null @@ -1,18 +0,0 @@ -// 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 2023-present Datadog, Inc. - -// Package status exposes the expvars we use for status tracking to the -// component system. -package status - -// team: network-device-monitoring - -// Component is the component type. -type Component interface { - AddTrapsPackets(int64) - GetTrapsPackets() int64 - AddTrapsPacketsAuthErrors(int64) - GetTrapsPacketsAuthErrors() int64 -} diff --git a/comp/snmptraps/config/config.go b/pkg/snmp/traps/config/config.go similarity index 93% rename from comp/snmptraps/config/config.go rename to pkg/snmp/traps/config/config.go index 2b839b226f6e5f..8b23a76b229745 100644 --- a/comp/snmptraps/config/config.go +++ b/pkg/snmp/traps/config/config.go @@ -3,6 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2020-present Datadog, Inc. +// Package config implements the configuration type for the traps server. package config import ( @@ -14,17 +15,11 @@ import ( "github.com/DataDog/datadog-agent/comp/core/config" "github.com/DataDog/datadog-agent/comp/core/log" - "github.com/DataDog/datadog-agent/comp/snmptraps/snmplog" "github.com/DataDog/datadog-agent/pkg/snmp/gosnmplib" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/snmplog" "github.com/DataDog/datadog-agent/pkg/snmp/utils" ) -const ( - defaultPort = uint16(9162) // Standard UDP port for traps. - defaultStopTimeout = 5 - packetsChanSize = 100 -) - // UserV3 contains the definition of one SNMPv3 user with its username and its auth // parameters. type UserV3 struct { @@ -55,6 +50,10 @@ func ReadConfig(host string, conf config.Component) (*TrapsConfig, error) { if err != nil { return nil, err } + + if !c.Enabled { + return nil, errors.New("traps listener is disabled") + } if err := c.SetDefaults(host, conf.GetString("network_devices.namespace")); err != nil { return c, err } @@ -80,6 +79,7 @@ func (c *TrapsConfig) SetDefaults(host string, namespace string) error { if c.StopTimeout == 0 { c.StopTimeout = defaultStopTimeout } + if host == "" { // Make sure to have at least some unique bytes for the authoritative engineID. // Unlikely to happen since the agent cannot start without a hostname @@ -164,8 +164,3 @@ func (c *TrapsConfig) BuildSNMPParams(logger log.Component) (*gosnmp.GoSNMP, err func (c *TrapsConfig) GetPacketChannelSize() int { return packetsChanSize } - -// IsEnabled returns whether SNMP trap collection is enabled in the Agent configuration. -func IsEnabled(conf config.Component) bool { - return conf.GetBool("network_devices.snmp_traps.enabled") -} diff --git a/pkg/snmp/traps/config/config_test.go b/pkg/snmp/traps/config/config_test.go new file mode 100644 index 00000000000000..ad380588300c16 --- /dev/null +++ b/pkg/snmp/traps/config/config_test.go @@ -0,0 +1,172 @@ +// 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 2020-present Datadog, Inc. + +package config + +import ( + "strings" + "testing" + + "github.com/DataDog/datadog-agent/comp/core/config" + "github.com/DataDog/datadog-agent/comp/core/log" + ddconf "github.com/DataDog/datadog-agent/pkg/config" + "github.com/DataDog/datadog-agent/pkg/util/fxutil" + "github.com/gosnmp/gosnmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +const mockedHostname = "VeryLongHostnameThatDoesNotFitIntoTheByteArray" + +var expectedEngineID = "\x80\xff\xff\xff\xff\x67\xb2\x0f\xe4\xdf\x73\x7a\xce\x28\x47\x03\x8f\x57\xe6\x5c\x98" + +var expectedEngineIDs = map[string]string{ + "VeryLongHostnameThatDoesNotFitIntoTheByteArray": "\x80\xff\xff\xff\xff\x67\xb2\x0f\xe4\xdf\x73\x7a\xce\x28\x47\x03\x8f\x57\xe6\x5c\x98", + "VeryLongHostnameThatIsDifferent": "\x80\xff\xff\xff\xff\xe7\x21\xcc\xd7\x0b\xe1\x60\xc5\x18\xd7\xde\x17\x86\xb0\x7d\x36", +} + +func makeConfig(t *testing.T, trapConfig TrapsConfig) config.Component { + return makeConfigWithGlobalNamespace(t, trapConfig, "") +} + +func makeConfigWithGlobalNamespace(t *testing.T, trapConfig TrapsConfig, globalNamespace string) config.Component { + trapConfig.Enabled = true + conf := ddconf.SetupConf() + if globalNamespace != "" { + conf.SetWithoutSource("network_devices.namespace", globalNamespace) + } + conf.SetConfigType("yaml") + yamlData := map[string]map[string]interface{}{ + "network_devices": { + "snmp_traps": trapConfig, + }, + } + out, err := yaml.Marshal(yamlData) + require.NoError(t, err) + err = conf.ReadConfig(strings.NewReader(string(out))) + require.NoError(t, err) + return conf +} + +func TestFullConfig(t *testing.T) { + logger := fxutil.Test[log.Component](t, log.MockModule) + rootConfig := makeConfig(t, TrapsConfig{ + Port: 1234, + Users: []UserV3{ + { + Username: "user", + AuthKey: "password", + AuthProtocol: "MD5", + PrivKey: "password", + PrivProtocol: "AES", + }, + }, + BindHost: "127.0.0.1", + CommunityStrings: []string{"public"}, + StopTimeout: 12, + Namespace: "foo", + }) + config, err := ReadConfig(mockedHostname, rootConfig) + assert.NoError(t, err) + assert.Equal(t, uint16(1234), config.Port) + assert.Equal(t, 12, config.StopTimeout) + assert.Equal(t, []string{"public"}, config.CommunityStrings) + assert.Equal(t, "127.0.0.1", config.BindHost) + assert.Equal(t, "foo", config.Namespace) + assert.Equal(t, []UserV3{ + { + Username: "user", + AuthKey: "password", + AuthProtocol: "MD5", + PrivKey: "password", + PrivProtocol: "AES", + }, + }, config.Users) + + params, err := config.BuildSNMPParams(logger) + assert.NoError(t, err) + assert.Equal(t, uint16(1234), params.Port) + assert.Equal(t, gosnmp.Version3, params.Version) + assert.Equal(t, "udp", params.Transport) + assert.NotNil(t, params.Logger) + assert.Equal(t, gosnmp.UserSecurityModel, params.SecurityModel) + assert.Equal(t, &gosnmp.UsmSecurityParameters{ + UserName: "user", + AuthoritativeEngineID: expectedEngineID, + AuthenticationProtocol: gosnmp.MD5, + AuthenticationPassphrase: "password", + PrivacyProtocol: gosnmp.AES, + PrivacyPassphrase: "password", + }, params.SecurityParameters) +} + +func TestMinimalConfig(t *testing.T) { + logger := fxutil.Test[log.Component](t, log.MockModule) + config, err := ReadConfig("", makeConfig(t, TrapsConfig{})) + assert.NoError(t, err) + assert.Equal(t, uint16(9162), config.Port) + assert.Equal(t, 5, config.StopTimeout) + assert.Empty(t, config.CommunityStrings) + assert.Equal(t, "0.0.0.0", config.BindHost) + assert.Empty(t, config.Users) + assert.Equal(t, "default", config.Namespace) + + params, err := config.BuildSNMPParams(logger) + assert.NoError(t, err) + assert.Equal(t, uint16(9162), params.Port) + assert.Equal(t, gosnmp.Version2c, params.Version) + assert.Equal(t, "udp", params.Transport) + assert.NotNil(t, params.Logger) + assert.Equal(t, nil, params.SecurityParameters) +} + +func TestDefaultUsers(t *testing.T) { + config, err := ReadConfig("", makeConfig(t, TrapsConfig{ + CommunityStrings: []string{"public"}, + StopTimeout: 11, + })) + assert.NoError(t, err) + + assert.Equal(t, 11, config.StopTimeout) +} + +func TestBuildAuthoritativeEngineID(t *testing.T) { + for hostname, engineID := range expectedEngineIDs { + config, err := ReadConfig(hostname, makeConfig(t, TrapsConfig{})) + assert.NoError(t, err) + assert.Equal(t, engineID, config.authoritativeEngineID) + } +} + +func TestNamespaceIsNormalized(t *testing.T) { + config, err := ReadConfig("", makeConfig(t, TrapsConfig{ + Namespace: "><\n\r\tfoo", + })) + assert.NoError(t, err) + + assert.Equal(t, "--foo", config.Namespace) +} + +func TestInvalidNamespace(t *testing.T) { + _, err := ReadConfig("", makeConfig(t, TrapsConfig{ + Namespace: strings.Repeat("x", 101), + })) + assert.Error(t, err) +} + +func TestNamespaceSetGlobally(t *testing.T) { + config, err := ReadConfig("", makeConfigWithGlobalNamespace(t, TrapsConfig{}, "foo")) + assert.NoError(t, err) + + assert.Equal(t, "foo", config.Namespace) +} + +func TestNamespaceSetBothGloballyAndLocally(t *testing.T) { + config, err := ReadConfig("", makeConfigWithGlobalNamespace(t, TrapsConfig{Namespace: "bar"}, "foo")) + assert.NoError(t, err) + + assert.Equal(t, "bar", config.Namespace) +} diff --git a/pkg/snmp/traps/config/constants.go b/pkg/snmp/traps/config/constants.go new file mode 100644 index 00000000000000..778151a615d7e4 --- /dev/null +++ b/pkg/snmp/traps/config/constants.go @@ -0,0 +1,12 @@ +// 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 config + +const ( + defaultPort = uint16(9162) // Standard UDP port for traps. + defaultStopTimeout = 5 + packetsChanSize = 100 +) diff --git a/comp/snmptraps/formatter/formatterimpl/formatter.go b/pkg/snmp/traps/formatter/formatter.go similarity index 89% rename from comp/snmptraps/formatter/formatterimpl/formatter.go rename to pkg/snmp/traps/formatter/formatter.go index be15b61dc7d2f4..0ad694efb36ac1 100644 --- a/comp/snmptraps/formatter/formatterimpl/formatter.go +++ b/pkg/snmp/traps/formatter/formatter.go @@ -3,28 +3,21 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2016-present Datadog, Inc. -// Package formatterimpl implements the formatter component. -package formatterimpl +// Package formatter provides tools for formatting SNMP traps. +package formatter import ( "encoding/json" "fmt" "strings" - "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexer" - "github.com/DataDog/datadog-agent/comp/core/log" - "github.com/DataDog/datadog-agent/comp/snmptraps/formatter" - "github.com/DataDog/datadog-agent/comp/snmptraps/oidresolver" - "github.com/DataDog/datadog-agent/comp/snmptraps/packet" "github.com/DataDog/datadog-agent/pkg/aggregator/sender" - "github.com/DataDog/datadog-agent/pkg/util/fxutil" + oidresolver "github.com/DataDog/datadog-agent/pkg/snmp/traps/oid_resolver" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/packet" + "github.com/gosnmp/gosnmp" - "go.uber.org/fx" -) -// Module implements the formatter component. -var Module = fxutil.Component( - fx.Provide(newJSONFormatter), + "github.com/DataDog/datadog-agent/comp/core/log" ) const ( @@ -32,9 +25,14 @@ const ( genericTrapOid = "1.3.6.1.6.3.1.1.5" ) +// Formatter is an interface to extract and format raw SNMP Traps +type Formatter interface { + FormatPacket(packet *packet.SnmpPacket) ([]byte, error) +} + // JSONFormatter is a Formatter implementation that transforms Traps into JSON type JSONFormatter struct { - oidResolver oidresolver.Component + oidResolver oidresolver.OIDResolver sender sender.Sender logger log.Component } @@ -54,11 +52,10 @@ const ( telemetryIncorrectFormat = "datadog.snmp_traps.incorrect_format" ) -// newJSONFormatter creates a new JSONFormatter instance with an optional OIDResolver variable. -func newJSONFormatter(oidResolver oidresolver.Component, demux demultiplexer.Component, logger log.Component) (formatter.Component, error) { - sender, err := demux.GetDefaultSender() - if err != nil { - return nil, err +// NewJSONFormatter creates a new JSONFormatter instance with an optional OIDResolver variable. +func NewJSONFormatter(oidResolver oidresolver.OIDResolver, sender sender.Sender, logger log.Component) (JSONFormatter, error) { + if oidResolver == nil { + return JSONFormatter{}, fmt.Errorf("NewJSONFormatter called with a nil OIDResolver") } return JSONFormatter{oidResolver, sender, logger}, nil } @@ -66,24 +63,24 @@ func newJSONFormatter(oidResolver oidresolver.Component, demux demultiplexer.Com // FormatPacket converts a raw SNMP trap packet to a FormattedSnmpPacket containing the JSON data and the tags to attach // // { -// "trap": { -// "ddsource": "snmp-traps", -// "ddtags": "namespace:default,snmp_device:10.0.0.2,...", -// "timestamp": 123456789, -// "snmpTrapName": "...", -// "snmpTrapOID": "1.3.6.1.5.3.....", -// "snmpTrapMIB": "...", -// "uptime": "12345", -// "genericTrap": "5", # v1 only -// "specificTrap": "0", # v1 only -// "variables": [ -// { -// "oid": "1.3.4.1....", -// "type": "integer", -// "value": 12 -// }, -// ... -// ], +// "trap": { +// "ddsource": "snmp-traps", +// "ddtags": "namespace:default,snmp_device:10.0.0.2,...", +// "timestamp": 123456789, +// "snmpTrapName": "...", +// "snmpTrapOID": "1.3.6.1.5.3.....", +// "snmpTrapMIB": "...", +// "uptime": "12345", +// "genericTrap": "5", # v1 only +// "specificTrap": "0", # v1 only +// "variables": [ +// { +// "oid": "1.3.4.1....", +// "type": "integer", +// "value": 12 +// }, +// ... +// ], // } // } func (f JSONFormatter) FormatPacket(packet *packet.SnmpPacket) ([]byte, error) { diff --git a/comp/snmptraps/formatter/formatterimpl/formatter_test.go b/pkg/snmp/traps/formatter/formatter_test.go similarity index 94% rename from comp/snmptraps/formatter/formatterimpl/formatter_test.go rename to pkg/snmp/traps/formatter/formatter_test.go index dc1b716444baf3..e3e3329b02b9c4 100644 --- a/comp/snmptraps/formatter/formatterimpl/formatter_test.go +++ b/pkg/snmp/traps/formatter/formatter_test.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2020-present Datadog, Inc. -package formatterimpl +package formatter import ( "encoding/base64" @@ -12,14 +12,10 @@ import ( "testing" "github.com/DataDog/datadog-agent/comp/core/log" - "github.com/DataDog/datadog-agent/comp/snmptraps/formatter" - "github.com/DataDog/datadog-agent/comp/snmptraps/oidresolver" - "github.com/DataDog/datadog-agent/comp/snmptraps/oidresolver/oidresolverimpl" - "github.com/DataDog/datadog-agent/comp/snmptraps/packet" - "github.com/DataDog/datadog-agent/comp/snmptraps/senderhelper" "github.com/DataDog/datadog-agent/pkg/aggregator/mocksender" + oidresolver "github.com/DataDog/datadog-agent/pkg/snmp/traps/oid_resolver" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/packet" "github.com/DataDog/datadog-agent/pkg/util/fxutil" - "go.uber.org/fx" "github.com/google/go-cmp/cmp" "github.com/gosnmp/gosnmp" @@ -214,16 +210,12 @@ var ( } ) -var testOptions = fx.Options( - log.MockModule, - senderhelper.Opts, - oidresolverimpl.MockModule, - Module, -) - func TestFormatPacketV1Generic(t *testing.T) { - defaultFormatter := fxutil.Test[formatter.Component](t, testOptions) + mockSender := mocksender.NewMockSender("snmp-traps-telemetry") + mockSender.SetupAcceptAll() + logger := fxutil.Test[log.Component](t, log.MockModule) + defaultFormatter, _ := NewJSONFormatter(oidresolver.NewMockResolver(), mockSender, logger) packet := packet.CreateTestV1GenericPacket() formattedPacket, err := defaultFormatter.FormatPacket(packet) require.NoError(t, err) @@ -267,7 +259,12 @@ func TestFormatPacketV1Generic(t *testing.T) { } func TestFormatPacketV1Specific(t *testing.T) { - defaultFormatter := fxutil.Test[formatter.Component](t, testOptions) + logger := fxutil.Test[log.Component](t, log.MockModule) + + mockSender := mocksender.NewMockSender("snmp-traps-telemetry") + mockSender.SetupAcceptAll() + + defaultFormatter, _ := NewJSONFormatter(oidresolver.NewMockResolver(), mockSender, logger) packet := packet.CreateTestV1SpecificPacket() formattedPacket, err := defaultFormatter.FormatPacket(packet) require.NoError(t, err) @@ -307,7 +304,11 @@ func TestFormatPacketV1Specific(t *testing.T) { } func TestFormatPacketToJSON(t *testing.T) { - defaultFormatter := fxutil.Test[formatter.Component](t, testOptions) + logger := fxutil.Test[log.Component](t, log.MockModule) + mockSender := mocksender.NewMockSender("snmp-traps-telemetry") + mockSender.SetupAcceptAll() + + defaultFormatter, _ := NewJSONFormatter(oidresolver.NewMockResolver(), mockSender, logger) packet := packet.CreateTestPacket(packet.NetSNMPExampleHeartbeatNotification) formattedPacket, err := defaultFormatter.FormatPacket(packet) @@ -340,7 +341,11 @@ func TestFormatPacketToJSON(t *testing.T) { } func TestFormatPacketToJSONShouldFailIfNotEnoughVariables(t *testing.T) { - defaultFormatter := fxutil.Test[formatter.Component](t, testOptions) + logger := fxutil.Test[log.Component](t, log.MockModule) + mockSender := mocksender.NewMockSender("snmp-traps-telemetry") + mockSender.SetupAcceptAll() + + defaultFormatter, _ := NewJSONFormatter(oidresolver.NewMockResolver(), mockSender, logger) packet := packet.CreateTestPacket(packet.NetSNMPExampleHeartbeatNotification) packet.Content.Variables = []gosnmp.SnmpPDU{ @@ -369,13 +374,19 @@ func TestFormatPacketToJSONShouldFailIfNotEnoughVariables(t *testing.T) { } func TestNewJSONFormatterWithNilStillWorks(t *testing.T) { - formatter := fxutil.Test[formatter.Component](t, testOptions) + logger := fxutil.Test[log.Component](t, log.MockModule) + mockSender := mocksender.NewMockSender("snmp-traps-telemetry") + mockSender.SetupAcceptAll() + + var formatter, err = NewJSONFormatter(oidresolver.NewMockResolver(), mockSender, logger) + require.NoError(t, err) packet := packet.CreateTestPacket(packet.NetSNMPExampleHeartbeatNotification) - _, err := formatter.FormatPacket(packet) + _, err = formatter.FormatPacket(packet) require.NoError(t, err) } func TestFormatterWithResolverAndTrapV2(t *testing.T) { + data := []struct { description string trap gosnmp.SnmpTrap @@ -701,7 +712,13 @@ func TestFormatterWithResolverAndTrapV2(t *testing.T) { }, } - formatter := fxutil.Test[formatter.Component](t, testOptions) + mockSender := mocksender.NewMockSender("snmp-traps-telemetry") + mockSender.SetupAcceptAll() + logger := fxutil.Test[log.Component](t, log.MockModule) + resolver := oidresolver.NewMockResolver() + formatter, err := NewJSONFormatter(resolver, mockSender, logger) + require.NoError(t, err) + for _, d := range data { t.Run(d.description, func(t *testing.T) { packet := packet.CreateTestPacket(d.trap) @@ -721,6 +738,7 @@ func TestFormatterWithResolverAndTrapV2(t *testing.T) { } func TestFormatterWithResolverAndTrapV1Generic(t *testing.T) { + logger := fxutil.Test[log.Component](t, log.MockModule) myFakeVarTypeExpected := []interface{}{ "test0", "test1", @@ -734,7 +752,11 @@ func TestFormatterWithResolverAndTrapV1Generic(t *testing.T) { "test130", } - formatter := fxutil.Test[formatter.Component](t, testOptions) + mockSender := mocksender.NewMockSender("snmp-traps-telemetry") + mockSender.SetupAcceptAll() + + formatter, err := NewJSONFormatter(oidresolver.NewMockResolver(), mockSender, logger) + require.NoError(t, err) packet := packet.CreateTestV1GenericPacket() data, err := formatter.FormatPacket(packet) require.NoError(t, err) @@ -1222,11 +1244,12 @@ func TestFormatterTelemetry(t *testing.T) { }, } - var mockSender *mocksender.MockSender - formatter := fxutil.Test[formatter.Component](t, - testOptions, - fx.Populate(&mockSender), - ) + logger := fxutil.Test[log.Component](t, log.MockModule) + mockSender := mocksender.NewMockSender("snmp-traps-telemetry") + mockSender.SetupAcceptAll() + resolver := oidresolver.NewMockResolver() + formatter, err := NewJSONFormatter(resolver, mockSender, logger) + require.NoError(t, err) for _, d := range data { t.Run(d.description, func(t *testing.T) { diff --git a/comp/snmptraps/formatter/formatterimpl/mock.go b/pkg/snmp/traps/formatter/mock.go similarity index 59% rename from comp/snmptraps/formatter/formatterimpl/mock.go rename to pkg/snmp/traps/formatter/mock.go index a3be52775c4ec3..5a0653dcaf03e7 100644 --- a/comp/snmptraps/formatter/formatterimpl/mock.go +++ b/pkg/snmp/traps/formatter/mock.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2023-present Datadog, Inc. -package formatterimpl +package formatter import ( "bytes" @@ -11,27 +11,14 @@ import ( "encoding/gob" "encoding/hex" - "github.com/DataDog/datadog-agent/comp/snmptraps/formatter" - "github.com/DataDog/datadog-agent/comp/snmptraps/packet" - "github.com/DataDog/datadog-agent/pkg/util/fxutil" - "go.uber.org/fx" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/packet" ) -// MockModule provides a dummy formatter that just hashes packets. -var MockModule = fxutil.Component( - fx.Provide(newDummy), -) - -// newDummy creates a new dummy formatter. -func newDummy() formatter.Component { - return &dummyFormatter{} -} - -// dummyFormatter is a formatter that just hashes packets. -type dummyFormatter struct{} +// DummyFormatter is a formatter that just hashes packets. +type DummyFormatter struct{} // FormatPacket is a dummy formatter method that hashes an SnmpPacket object -func (f dummyFormatter) FormatPacket(packet *packet.SnmpPacket) ([]byte, error) { +func (f DummyFormatter) FormatPacket(packet *packet.SnmpPacket) ([]byte, error) { var b bytes.Buffer for _, err := range []error{ gob.NewEncoder(&b).Encode(packet.Addr), diff --git a/pkg/snmp/traps/forwarder/forwarder.go b/pkg/snmp/traps/forwarder/forwarder.go new file mode 100644 index 00000000000000..2bb5cb0cf91c80 --- /dev/null +++ b/pkg/snmp/traps/forwarder/forwarder.go @@ -0,0 +1,82 @@ +// 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 2022-present Datadog, Inc. + +// Package forwarder defines a type that receives trap data from the +// listener, formats it properly, and sends it to the backend. +package forwarder + +import ( + "time" + + "github.com/DataDog/datadog-agent/comp/core/log" + "github.com/DataDog/datadog-agent/pkg/aggregator/sender" + "github.com/DataDog/datadog-agent/pkg/epforwarder" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/formatter" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/packet" +) + +// TrapForwarder consumes from a trapsIn channel, format traps and send them as EventPlatformEvents +// The TrapForwarder is an intermediate step between the listener and the epforwarder in order to limit the processing of the listener +// to the minimum. The forwarder process payloads received by the listener via the trapsIn channel, formats them and finally +// give them to the epforwarder for sending it to Datadog. +type TrapForwarder struct { + trapsIn packet.PacketsChannel + formatter formatter.Formatter + sender sender.Sender + stopChan chan struct{} + logger log.Component +} + +// NewTrapForwarder creates a simple TrapForwarder instance +func NewTrapForwarder(formatter formatter.Formatter, sender sender.Sender, packets packet.PacketsChannel, logger log.Component) (*TrapForwarder, error) { + return &TrapForwarder{ + trapsIn: packets, + formatter: formatter, + sender: sender, + stopChan: make(chan struct{}, 1), + logger: logger, + }, nil +} + +// Start the TrapForwarder instance. Need to Stop it manually. +func (tf *TrapForwarder) Start() { + tf.logger.Info("Starting TrapForwarder") + go tf.run() +} + +// Stop the TrapForwarder instance. +func (tf *TrapForwarder) Stop() { + select { + case tf.stopChan <- struct{}{}: + default: + tf.logger.Warn("TrapForwarder stopped twice.") + } +} + +func (tf *TrapForwarder) run() { + flushTicker := time.NewTicker(10 * time.Second).C + for { + select { + case <-tf.stopChan: + tf.logger.Info("Stopped TrapForwarder") + return + case packet := <-tf.trapsIn: + tf.sendTrap(packet) + case <-flushTicker: + tf.sender.Commit() // Commit metrics + } + } +} + +func (tf *TrapForwarder) sendTrap(packet *packet.SnmpPacket) { + data, err := tf.formatter.FormatPacket(packet) + if err != nil { + tf.logger.Errorf("failed to format packet: %s", err) + return + } + tf.logger.Tracef("send trap payload: %s", string(data)) + tf.sender.Count("datadog.snmp_traps.forwarded", 1, "", packet.GetTags()) + tf.sender.EventPlatformEvent(data, epforwarder.EventTypeSnmpTraps) +} diff --git a/pkg/snmp/traps/forwarder/forwarder_test.go b/pkg/snmp/traps/forwarder/forwarder_test.go new file mode 100644 index 00000000000000..194aefb68e2edd --- /dev/null +++ b/pkg/snmp/traps/forwarder/forwarder_test.go @@ -0,0 +1,102 @@ +// 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 2022-present Datadog, Inc. + +package forwarder + +import ( + "net" + "testing" + "time" + + "github.com/gosnmp/gosnmp" + "github.com/stretchr/testify/require" + + "github.com/DataDog/datadog-agent/comp/core/log" + "github.com/DataDog/datadog-agent/pkg/aggregator/mocksender" + "github.com/DataDog/datadog-agent/pkg/epforwarder" + + "github.com/DataDog/datadog-agent/pkg/snmp/traps/formatter" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/packet" + "github.com/DataDog/datadog-agent/pkg/util/fxutil" +) + +var simpleUDPAddr = &net.UDPAddr{IP: net.IPv4(1, 1, 1, 1), Port: 161} + +func createForwarder(t *testing.T) (forwarder *TrapForwarder, err error) { + logger := fxutil.Test[log.Component](t, log.MockModule) + packetsIn := make(packet.PacketsChannel) + mockSender := mocksender.NewMockSender("snmp-traps-listener") + mockSender.SetupAcceptAll() + + forwarder, err = NewTrapForwarder(&formatter.DummyFormatter{}, mockSender, packetsIn, logger) + if err != nil { + return nil, err + } + forwarder.Start() + t.Cleanup(func() { forwarder.Stop() }) + return forwarder, err +} + +func makeSnmpPacket(trap gosnmp.SnmpTrap) *packet.SnmpPacket { + gosnmpPacket := &gosnmp.SnmpPacket{ + Version: gosnmp.Version2c, + Community: "public", + Variables: trap.Variables, + SnmpTrap: trap, + } + return &packet.SnmpPacket{ + Content: gosnmpPacket, + Addr: simpleUDPAddr, + Namespace: "totoro", + Timestamp: time.Now().UnixMilli()} +} + +func TestV1GenericTrapAreForwarder(t *testing.T) { + forwarder, err := createForwarder(t) + require.NoError(t, err) + sender, ok := forwarder.sender.(*mocksender.MockSender) + require.True(t, ok) + packet := makeSnmpPacket(packet.LinkDownv1GenericTrap) + rawEvent, err := forwarder.formatter.FormatPacket(packet) + require.NoError(t, err) + forwarder.trapsIn <- packet + time.Sleep(100 * time.Millisecond) + sender.AssertEventPlatformEvent(t, rawEvent, epforwarder.EventTypeSnmpTraps) +} + +func TestV1SpecificTrapAreForwarder(t *testing.T) { + forwarder, err := createForwarder(t) + require.NoError(t, err) + sender, ok := forwarder.sender.(*mocksender.MockSender) + require.True(t, ok) + packet := makeSnmpPacket(packet.AlarmActiveStatev1SpecificTrap) + rawEvent, err := forwarder.formatter.FormatPacket(packet) + require.NoError(t, err) + forwarder.trapsIn <- packet + time.Sleep(100 * time.Millisecond) + sender.AssertEventPlatformEvent(t, rawEvent, epforwarder.EventTypeSnmpTraps) +} +func TestV2TrapAreForwarder(t *testing.T) { + forwarder, err := createForwarder(t) + require.NoError(t, err) + sender, ok := forwarder.sender.(*mocksender.MockSender) + require.True(t, ok) + packet := makeSnmpPacket(packet.NetSNMPExampleHeartbeatNotification) + rawEvent, err := forwarder.formatter.FormatPacket(packet) + require.NoError(t, err) + forwarder.trapsIn <- packet + time.Sleep(100 * time.Millisecond) + sender.AssertEventPlatformEvent(t, rawEvent, epforwarder.EventTypeSnmpTraps) +} + +func TestForwarderTelemetry(t *testing.T) { + forwarder, err := createForwarder(t) + require.NoError(t, err) + sender, ok := forwarder.sender.(*mocksender.MockSender) + require.True(t, ok) + forwarder.trapsIn <- makeSnmpPacket(packet.NetSNMPExampleHeartbeatNotification) + time.Sleep(100 * time.Millisecond) + sender.AssertMetric(t, "Count", "datadog.snmp_traps.forwarded", 1, "", []string{"snmp_device:1.1.1.1", "device_namespace:totoro", "snmp_version:2"}) +} diff --git a/comp/snmptraps/listener/listenerimpl/listener.go b/pkg/snmp/traps/listener/listener.go similarity index 52% rename from comp/snmptraps/listener/listenerimpl/listener.go rename to pkg/snmp/traps/listener/listener.go index 20ac918435f481..ba63f391dd3ad0 100644 --- a/comp/snmptraps/listener/listenerimpl/listener.go +++ b/pkg/snmp/traps/listener/listener.go @@ -3,111 +3,75 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2022-present Datadog, Inc. -// Package listenerimpl implements the Listener component. -package listenerimpl +// Package listener listens for SNMP messages, parses them, and publishes messages on a channel. +package listener import ( - "context" "errors" "fmt" "net" "time" "github.com/DataDog/datadog-agent/pkg/aggregator/sender" - "github.com/DataDog/datadog-agent/pkg/util/fxutil" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/config" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/packet" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/status" + "github.com/gosnmp/gosnmp" - "go.uber.org/fx" - "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexer" "github.com/DataDog/datadog-agent/comp/core/log" - "github.com/DataDog/datadog-agent/comp/snmptraps/config" - "github.com/DataDog/datadog-agent/comp/snmptraps/listener" - "github.com/DataDog/datadog-agent/comp/snmptraps/packet" - "github.com/DataDog/datadog-agent/comp/snmptraps/status" -) - -// Module defines the fx options for this component. -var Module = fxutil.Component( - fx.Provide(newTrapListener), ) -// trapListener opens an UDP socket and put all received traps in a channel -type trapListener struct { +// TrapListener opens an UDP socket and put all received traps in a channel +type TrapListener struct { config *config.TrapsConfig sender sender.Sender packets packet.PacketsChannel listener *gosnmp.TrapListener errorsChannel chan error logger log.Component - status status.Component -} - -type dependencies struct { - fx.In - Config config.Component - Demux demultiplexer.Component - Logger log.Component - Status status.Component + status status.Manager } -// newTrapListener creates a TrapListener and registers it with the lifecycle. -func newTrapListener(lc fx.Lifecycle, dep dependencies) (listener.Component, error) { - sender, err := dep.Demux.GetDefaultSender() - if err != nil { - return nil, err - } - config := dep.Config.Get() +// NewTrapListener creates a simple TrapListener instance but does not start it +func NewTrapListener(config *config.TrapsConfig, sender sender.Sender, packets packet.PacketsChannel, logger log.Component, status status.Manager) (*TrapListener, error) { + var err error gosnmpListener := gosnmp.NewTrapListener() - gosnmpListener.Params, err = config.BuildSNMPParams(dep.Logger) + gosnmpListener.Params, err = config.BuildSNMPParams(logger) if err != nil { return nil, err } errorsChan := make(chan error, 1) - trapListener := &trapListener{ + trapListener := &TrapListener{ config: config, sender: sender, - packets: make(packet.PacketsChannel, config.GetPacketChannelSize()), + packets: packets, listener: gosnmpListener, errorsChannel: errorsChan, - logger: dep.Logger, - status: dep.Status, + logger: logger, + status: status, } gosnmpListener.OnNewTrap = trapListener.receiveTrap - if config.Enabled { - lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - return trapListener.start() - }, - OnStop: func(ctx context.Context) error { - return trapListener.stop() - }, - }) - } - return trapListener, nil } -// Packets returns the packets channel to which the listener publishes. -func (t *trapListener) Packets() packet.PacketsChannel { - return t.packets -} - -// start the TrapListener instance. -func (t *trapListener) start() error { +// Start the TrapListener instance. Need to be manually Stopped +func (t *TrapListener) Start() error { t.logger.Infof("Start listening for traps on %s", t.config.Addr()) go t.run() return t.blockUntilReady() } -func (t *trapListener) run() { +func (t *TrapListener) run() { err := t.listener.Listen(t.config.Addr()) // blocking call if err != nil { t.errorsChannel <- err } + } -func (t *trapListener) blockUntilReady() error { +func (t *TrapListener) blockUntilReady() error { select { // Wait for listener to be started and listening to traps. // See: https://godoc.org/github.com/gosnmp/gosnmp#TrapListener.Listening @@ -120,26 +84,12 @@ func (t *trapListener) blockUntilReady() error { } } -// stop the current TrapListener instance -func (t *trapListener) stop() error { - - stopped := make(chan interface{}) - - go func() { - t.logger.Infof("Stop listening on %s", t.config.Addr()) - t.listener.Close() - close(stopped) - }() - - select { - case <-stopped: - case <-time.After(time.Duration(t.config.StopTimeout) * time.Second): - return fmt.Errorf("TrapListener.Stop() timed out after %d seconds", t.config.StopTimeout) - } - return nil +// Stop the current TrapListener instance +func (t *TrapListener) Stop() { + t.listener.Close() } -func (t *trapListener) receiveTrap(p *gosnmp.SnmpPacket, u *net.UDPAddr) { +func (t *TrapListener) receiveTrap(p *gosnmp.SnmpPacket, u *net.UDPAddr) { packet := &packet.SnmpPacket{Content: p, Addr: u, Timestamp: time.Now().UnixMilli(), Namespace: t.config.Namespace} tags := packet.GetTags() diff --git a/comp/snmptraps/listener/listenerimpl/listener_test.go b/pkg/snmp/traps/listener/listener_test.go similarity index 60% rename from comp/snmptraps/listener/listenerimpl/listener_test.go rename to pkg/snmp/traps/listener/listener_test.go index 1f93a2080a9bd2..95e8842809066c 100644 --- a/comp/snmptraps/listener/listenerimpl/listener_test.go +++ b/pkg/snmp/traps/listener/listener_test.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2022-present Datadog, Inc. -package listenerimpl +package listener import ( "errors" @@ -11,16 +11,11 @@ import ( "time" "github.com/DataDog/datadog-agent/comp/core/log" - "github.com/DataDog/datadog-agent/comp/snmptraps/config" - "github.com/DataDog/datadog-agent/comp/snmptraps/config/configimpl" - "github.com/DataDog/datadog-agent/comp/snmptraps/listener" - packetModule "github.com/DataDog/datadog-agent/comp/snmptraps/packet" - "github.com/DataDog/datadog-agent/comp/snmptraps/senderhelper" - "github.com/DataDog/datadog-agent/comp/snmptraps/status" - "github.com/DataDog/datadog-agent/comp/snmptraps/status/statusimpl" "github.com/DataDog/datadog-agent/pkg/aggregator/mocksender" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/config" + packetModule "github.com/DataDog/datadog-agent/pkg/snmp/traps/packet" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/status" "github.com/DataDog/datadog-agent/pkg/util/fxutil" - "go.uber.org/fx" "github.com/gosnmp/gosnmp" "github.com/stretchr/testify/assert" @@ -31,36 +26,28 @@ import ( const defaultTimeout = 1 * time.Second -type services struct { - fx.In - Config config.Component - Sender *mocksender.MockSender - Status status.Component - Listener listener.Component - Logger log.Component -} - -func listenerTestSetup(t *testing.T, conf *config.TrapsConfig) *services { - conf.Enabled = true - s := fxutil.Test[services](t, - log.MockModule, - configimpl.MockModule, - statusimpl.MockModule, - senderhelper.Opts, - Module, - fx.Replace(conf), - ) - return &s +func listenerTestSetup(t *testing.T, config *config.TrapsConfig) (*mocksender.MockSender, *TrapListener, status.Manager) { + logger := fxutil.Test[log.Component](t, log.MockModule) + mockSender := mocksender.NewMockSender("snmp-traps-telemetry") + mockSender.SetupAcceptAll() + packetOutChan := make(packetModule.PacketsChannel, config.GetPacketChannelSize()) + status := status.NewMock() + trapListener, err := NewTrapListener(config, mockSender, packetOutChan, logger, status) + require.NoError(t, err) + err = trapListener.Start() + require.NoError(t, err) + return mockSender, trapListener, status } func TestListenV1GenericTrap(t *testing.T) { serverPort, err := ndmtestutils.GetFreePort() require.NoError(t, err) config := &config.TrapsConfig{Port: serverPort, CommunityStrings: []string{"public"}, Namespace: "totoro"} - s := listenerTestSetup(t, config) + _, trapListener, status := listenerTestSetup(t, config) + defer trapListener.Stop() - sendTestV1GenericTrap(t, s.Config.Get(), "public") - packet, err := receivePacket(s, defaultTimeout) + sendTestV1GenericTrap(t, config, "public") + packet, err := receivePacket(t, trapListener, defaultTimeout, status) require.NoError(t, err) packet.Content.SnmpTrap.Variables = packet.Content.Variables assert.Equal(t, packetModule.LinkDownv1GenericTrap, packet.Content.SnmpTrap) @@ -70,10 +57,11 @@ func TestServerV1SpecificTrap(t *testing.T) { serverPort, err := ndmtestutils.GetFreePort() require.NoError(t, err) config := &config.TrapsConfig{Port: serverPort, CommunityStrings: []string{"public"}} - s := listenerTestSetup(t, config) + _, trapListener, status := listenerTestSetup(t, config) + defer trapListener.Stop() sendTestV1SpecificTrap(t, config, "public") - packet, err := receivePacket(s, defaultTimeout) + packet, err := receivePacket(t, trapListener, defaultTimeout, status) require.NoError(t, err) packet.Content.SnmpTrap.Variables = packet.Content.Variables assert.Equal(t, packetModule.AlarmActiveStatev1SpecificTrap, packet.Content.SnmpTrap) @@ -83,10 +71,11 @@ func TestServerV2(t *testing.T) { serverPort, err := ndmtestutils.GetFreePort() require.NoError(t, err) config := &config.TrapsConfig{Port: serverPort, CommunityStrings: []string{"public"}} - s := listenerTestSetup(t, config) + _, trapListener, status := listenerTestSetup(t, config) + defer trapListener.Stop() sendTestV2Trap(t, config, "public") - packet, err := receivePacket(s, defaultTimeout) + packet, err := receivePacket(t, trapListener, defaultTimeout, status) require.NoError(t, err) assertIsValidV2Packet(t, packet, config) assertVariables(t, packet) @@ -96,14 +85,15 @@ func TestServerV2BadCredentials(t *testing.T) { serverPort, err := ndmtestutils.GetFreePort() require.NoError(t, err) config := &config.TrapsConfig{Port: serverPort, CommunityStrings: []string{"public"}, Namespace: "totoro"} - s := listenerTestSetup(t, config) + mockSender, trapListener, status := listenerTestSetup(t, config) + defer trapListener.Stop() sendTestV2Trap(t, config, "wrong-community") - _, err2 := receivePacket(s, defaultTimeout) + _, err2 := receivePacket(t, trapListener, defaultTimeout, status) require.EqualError(t, err2, "invalid packet") - s.Sender.AssertMetric(t, "Count", "datadog.snmp_traps.received", 1, "", []string{"snmp_device:127.0.0.1", "device_namespace:totoro", "snmp_version:2"}) - s.Sender.AssertMetric(t, "Count", "datadog.snmp_traps.invalid_packet", 1, "", []string{"snmp_device:127.0.0.1", "device_namespace:totoro", "snmp_version:2", "reason:unknown_community_string"}) + mockSender.AssertMetric(t, "Count", "datadog.snmp_traps.received", 1, "", []string{"snmp_device:127.0.0.1", "device_namespace:totoro", "snmp_version:2"}) + mockSender.AssertMetric(t, "Count", "datadog.snmp_traps.invalid_packet", 1, "", []string{"snmp_device:127.0.0.1", "device_namespace:totoro", "snmp_version:2", "reason:unknown_community_string"}) } func TestServerV3(t *testing.T) { @@ -111,7 +101,8 @@ func TestServerV3(t *testing.T) { require.NoError(t, err) userV3 := config.UserV3{Username: "user", AuthKey: "password", AuthProtocol: "sha", PrivKey: "password", PrivProtocol: "aes"} config := &config.TrapsConfig{Port: serverPort, Users: []config.UserV3{userV3}} - s := listenerTestSetup(t, config) + _, trapListener, status := listenerTestSetup(t, config) + defer trapListener.Stop() sendTestV3Trap(t, config, &gosnmp.UsmSecurityParameters{ UserName: "user", @@ -121,7 +112,7 @@ func TestServerV3(t *testing.T) { PrivacyPassphrase: "password", PrivacyProtocol: gosnmp.AES, }) - packet, err := receivePacket(s, defaultTimeout) + packet, err := receivePacket(t, trapListener, defaultTimeout, status) require.NoError(t, err) assertVariables(t, packet) } @@ -131,7 +122,8 @@ func TestServerV3BadCredentials(t *testing.T) { require.NoError(t, err) userV3 := config.UserV3{Username: "user", AuthKey: "password", AuthProtocol: "sha", PrivKey: "password", PrivProtocol: "aes"} config := &config.TrapsConfig{Port: serverPort, Users: []config.UserV3{userV3}} - s := listenerTestSetup(t, config) + _, trapListener, _ := listenerTestSetup(t, config) + defer trapListener.Stop() sendTestV3Trap(t, config, &gosnmp.UsmSecurityParameters{ UserName: "user", @@ -141,22 +133,23 @@ func TestServerV3BadCredentials(t *testing.T) { PrivacyPassphrase: "wrong_password", PrivacyProtocol: gosnmp.AES, }) - assertNoPacketReceived(t, s.Listener) + assertNoPacketReceived(t, trapListener) } func TestListenerTrapsReceivedTelemetry(t *testing.T) { serverPort, err := ndmtestutils.GetFreePort() require.NoError(t, err) config := &config.TrapsConfig{Port: serverPort, CommunityStrings: []string{"public"}, Namespace: "totoro"} - s := listenerTestSetup(t, config) + mockSender, trapListener, status := listenerTestSetup(t, config) + defer trapListener.Stop() sendTestV1GenericTrap(t, config, "public") - _, err2 := receivePacket(s, defaultTimeout) // Wait fot + _, err2 := receivePacket(t, trapListener, defaultTimeout, status) // Wait for packet require.NoError(t, err2) - s.Sender.AssertMetric(t, "Count", "datadog.snmp_traps.received", 1, "", []string{"snmp_device:127.0.0.1", "device_namespace:totoro", "snmp_version:1"}) + mockSender.AssertMetric(t, "Count", "datadog.snmp_traps.received", 1, "", []string{"snmp_device:127.0.0.1", "device_namespace:totoro", "snmp_version:1"}) } -func receivePacket(s *services, timeoutDuration time.Duration) (*packetModule.SnmpPacket, error) { +func receivePacket(t *testing.T, listener *TrapListener, timeoutDuration time.Duration, status status.Manager) (*packetModule.SnmpPacket, error) { //nolint:revive // TODO fix revive unused-parameter timeout := time.After(timeoutDuration) ticker := time.NewTicker(20 * time.Millisecond) defer ticker.Stop() @@ -165,10 +158,10 @@ func receivePacket(s *services, timeoutDuration time.Duration) (*packetModule.Sn select { case <-timeout: return nil, errors.New("timeout waiting for trap") - case packet := <-s.Listener.Packets(): + case packet := <-listener.packets: return packet, nil case <-ticker.C: - if s.Status.GetTrapsPacketsAuthErrors() > 0 { + if status.GetTrapsPacketsAuthErrors() > 0 { // invalid packet/bad credentials return nil, errors.New("invalid packet") } @@ -176,9 +169,9 @@ func receivePacket(s *services, timeoutDuration time.Duration) (*packetModule.Sn } } -func assertNoPacketReceived(t *testing.T, listener listener.Component) { +func assertNoPacketReceived(t *testing.T, listener *TrapListener) { select { - case <-listener.Packets(): + case <-listener.packets: t.Error("Unexpectedly received an unauthorized packet") case <-time.After(100 * time.Millisecond): break diff --git a/comp/snmptraps/listener/listenerimpl/test_helpers.go b/pkg/snmp/traps/listener/test_helpers.go similarity index 95% rename from comp/snmptraps/listener/listenerimpl/test_helpers.go rename to pkg/snmp/traps/listener/test_helpers.go index 1d8f4026a1608f..1976ce4785ad22 100644 --- a/comp/snmptraps/listener/listenerimpl/test_helpers.go +++ b/pkg/snmp/traps/listener/test_helpers.go @@ -1,18 +1,18 @@ // 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 2022-present Datadog, Inc. +// Copyright 2020-present Datadog, Inc. -//go:build test +//go:build !serverless -package listenerimpl +package listener import ( "testing" "time" - "github.com/DataDog/datadog-agent/comp/snmptraps/config" - "github.com/DataDog/datadog-agent/comp/snmptraps/packet" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/config" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/packet" "github.com/gosnmp/gosnmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/pkg/snmp/traps/oid_resolver/mock.go b/pkg/snmp/traps/oid_resolver/mock.go new file mode 100644 index 00000000000000..c0919965294f40 --- /dev/null +++ b/pkg/snmp/traps/oid_resolver/mock.go @@ -0,0 +1,98 @@ +// 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 2022-present Datadog, Inc. + +package oidresolver + +import "fmt" + +// MockResolver implements OIDResolver with a mock database. +type MockResolver struct { + content *TrapDBFileContent +} + +// GetTrapMetadata implements OIDResolver#GetTrapMetadata. +func (r MockResolver) GetTrapMetadata(trapOid string) (TrapMetadata, error) { + trapOid = NormalizeOID(trapOid) + trapData, ok := r.content.Traps[trapOid] + if !ok { + return TrapMetadata{}, fmt.Errorf("trap OID %s is not defined", trapOid) + } + return trapData, nil +} + +// GetVariableMetadata implements OIDResolver#GetVariableMetadata. +func (r MockResolver) GetVariableMetadata(string, varOid string) (VariableMetadata, error) { //nolint:revive // TODO fix revive unusued-parameter + varOid = NormalizeOID(varOid) + varData, ok := r.content.Variables[varOid] + if !ok { + return VariableMetadata{}, fmt.Errorf("variable OID %s is not defined", varOid) + } + return varData, nil +} + +// NewMockResolver creates a mock resolver populated with fake data. +func NewMockResolver() *MockResolver { + return &MockResolver{&dummyTrapDB} +} + +var dummyTrapDB = TrapDBFileContent{ + Traps: TrapSpec{ + "1.3.6.1.6.3.1.1.5.3": TrapMetadata{Name: "ifDown", MIBName: "IF-MIB"}, // v1 Trap + "1.3.6.1.4.1.8072.2.3.0.1": TrapMetadata{Name: "netSnmpExampleHeartbeatNotification", MIBName: "NET-SNMP-EXAMPLES-MIB"}, // v2+ + "1.3.6.1.6.3.1.1.5.4": TrapMetadata{Name: "linkUp", MIBName: "IF-MIB"}, + }, + Variables: variableSpec{ + "1.3.6.1.2.1.2.2.1.1": VariableMetadata{Name: "ifIndex"}, + "1.3.6.1.2.1.2.2.1.7": VariableMetadata{Name: "ifAdminStatus", Enumeration: map[int]string{1: "up", 2: "down", 3: "testing"}}, + "1.3.6.1.2.1.2.2.1.8": VariableMetadata{Name: "ifOperStatus", Enumeration: map[int]string{1: "up", 2: "down", 3: "testing", 4: "unknown", 5: "dormant", 6: "notPresent", 7: "lowerLayerDown"}}, + "1.3.6.1.4.1.8072.2.3.2.1": VariableMetadata{Name: "netSnmpExampleHeartbeatRate"}, + "1.3.6.1.2.1.200.1.1.1.3": VariableMetadata{Name: "pwCepSonetConfigErrorOrStatus", Bits: map[int]string{ + 0: "other", + 1: "timeslotInUse", + 2: "timeslotMisuse", + 3: "peerDbaIncompatible", + 4: "peerEbmIncompatible", + 5: "peerRtpIncompatible", + 6: "peerAsyncIncompatible", + 7: "peerDbaAsymmetric", + 8: "peerEbmAsymmetric", + 9: "peerRtpAsymmetric", + 10: "peerAsyncAsymmetric", + }}, + "1.3.6.1.2.1.200.1.3.1.5": VariableMetadata{Name: "myFakeVarType", Bits: map[int]string{ + 0: "test0", + 1: "test1", + 3: "test3", + 5: "test5", + 6: "test6", + 12: "test12", + 15: "test15", + 130: "test130", + }}, + "1.3.6.1.2.1.200.1.3.1.6": VariableMetadata{ + Name: "myBadVarType", + Enumeration: map[int]string{ + 0: "test0", + 1: "test1", + 3: "test3", + 5: "test5", + 6: "test6", + 12: "test12", + 15: "test15", + 130: "test130", + }, + Bits: map[int]string{ + 0: "test0", + 1: "test1", + 3: "test3", + 5: "test5", + 6: "test6", + 12: "test12", + 15: "test15", + 130: "test130", + }, + }, + }, +} diff --git a/comp/snmptraps/oidresolver/oidresolverimpl/oid_resolver.go b/pkg/snmp/traps/oid_resolver/oid_resolver.go similarity index 70% rename from comp/snmptraps/oidresolver/oidresolverimpl/oid_resolver.go rename to pkg/snmp/traps/oid_resolver/oid_resolver.go index 33e39f635dc165..f32f7f49fe1d5f 100644 --- a/comp/snmptraps/oidresolver/oidresolverimpl/oid_resolver.go +++ b/pkg/snmp/traps/oid_resolver/oid_resolver.go @@ -3,8 +3,8 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2022-present Datadog, Inc. -// Package oidresolverimpl implements the OID Resolver component. -package oidresolverimpl +// Package oidresolver resolves OIDs to metadata about those OIDs. +package oidresolver import ( "compress/gzip" @@ -16,19 +16,11 @@ import ( "path/filepath" "sort" "strings" + "unicode" - "go.uber.org/fx" "gopkg.in/yaml.v2" - "github.com/DataDog/datadog-agent/comp/core/config" "github.com/DataDog/datadog-agent/comp/core/log" - "github.com/DataDog/datadog-agent/comp/snmptraps/oidresolver" - "github.com/DataDog/datadog-agent/pkg/util/fxutil" -) - -// Module defines the fx options for this component. -var Module = fxutil.Component( - fx.Provide(newResolver), ) const ddTrapDBFileNamePrefix string = "dd_traps_db" @@ -44,26 +36,28 @@ var nodesOIDThatShouldNeverMatch = []string{ type unmarshaller func(data []byte, v interface{}) error -// multiFilesOIDResolver is an OIDResolver implementation that can be configured with multiple input files. +// OIDResolver is a interface to get Trap and Variable metadata from OIDs +type OIDResolver interface { + GetTrapMetadata(trapOID string) (TrapMetadata, error) + GetVariableMetadata(trapOID string, varOID string) (VariableMetadata, error) +} + +// MultiFilesOIDResolver is an OIDResolver implementation that can be configured with multiple input files. // Trap OIDs conflicts are resolved using the name of the source file in alphabetical order and by giving // the less priority to Datadog's own database shipped with the agent. // Variable OIDs conflicts are fully resolved by also looking at the trap OID. A given trap OID only // exist in a single file (after the previous conflict resolution), meaning that we get the variable // metadata from that same file. -type multiFilesOIDResolver struct { - traps oidresolver.TrapSpec +type MultiFilesOIDResolver struct { + traps TrapSpec logger log.Component } -func newResolver(conf config.Component, logger log.Component) (oidresolver.Component, error) { - return newMultiFilesOIDResolver(conf.GetString("confd_path"), logger) -} - -// newMultiFilesOIDResolver creates a new MultiFilesOIDResolver instance by loading json or yaml files +// NewMultiFilesOIDResolver creates a new MultiFilesOIDResolver instance by loading json or yaml files // (optionnally gzipped) located in the directory snmp.d/traps_db/ -func newMultiFilesOIDResolver(confdPath string, logger log.Component) (*multiFilesOIDResolver, error) { - oidResolver := &multiFilesOIDResolver{ - traps: make(oidresolver.TrapSpec), +func NewMultiFilesOIDResolver(confdPath string, logger log.Component) (*MultiFilesOIDResolver, error) { + oidResolver := &MultiFilesOIDResolver{ + traps: make(TrapSpec), logger: logger, } trapsDBRoot := filepath.Join(confdPath, "snmp.d", "traps_db") @@ -85,11 +79,11 @@ func newMultiFilesOIDResolver(confdPath string, logger log.Component) (*multiFil } // GetTrapMetadata returns TrapMetadata for a given trapOID -func (or *multiFilesOIDResolver) GetTrapMetadata(trapOID string) (oidresolver.TrapMetadata, error) { - trapOID = strings.TrimSuffix(oidresolver.NormalizeOID(trapOID), ".0") +func (or *MultiFilesOIDResolver) GetTrapMetadata(trapOID string) (TrapMetadata, error) { + trapOID = strings.TrimSuffix(NormalizeOID(trapOID), ".0") trapData, ok := or.traps[trapOID] if !ok { - return oidresolver.TrapMetadata{}, fmt.Errorf("trap OID %s is not defined", trapOID) + return TrapMetadata{}, fmt.Errorf("trap OID %s is not defined", trapOID) } return trapData, nil } @@ -97,21 +91,21 @@ func (or *multiFilesOIDResolver) GetTrapMetadata(trapOID string) (oidresolver.Tr // GetVariableMetadata returns VariableMetadata for a given variableOID and trapOID. // The trapOID should not be needed in theory but the Datadog Agent allows to define multiple variable names for the // same OID as long as they are defined in different file. The trapOID is used to differentiate between these files. -func (or *multiFilesOIDResolver) GetVariableMetadata(trapOID string, varOID string) (oidresolver.VariableMetadata, error) { - trapOID = strings.TrimSuffix(oidresolver.NormalizeOID(trapOID), ".0") - varOID = strings.TrimSuffix(oidresolver.NormalizeOID(varOID), ".0") +func (or *MultiFilesOIDResolver) GetVariableMetadata(trapOID string, varOID string) (VariableMetadata, error) { + trapOID = strings.TrimSuffix(NormalizeOID(trapOID), ".0") + varOID = strings.TrimSuffix(NormalizeOID(varOID), ".0") trapData, ok := or.traps[trapOID] if !ok { - return oidresolver.VariableMetadata{}, fmt.Errorf("trap OID %s is not defined", trapOID) + return VariableMetadata{}, fmt.Errorf("trap OID %s is not defined", trapOID) } recreatedVarOID := varOID for { - varData, ok := trapData.VariableSpecPtr[recreatedVarOID] + varData, ok := trapData.variableSpecPtr[recreatedVarOID] if ok { - if varData.IsIntermediateNode { + if varData.isIntermediateNode { // Found a known Node while climibing up the tree, no chance of finding a match higher - return oidresolver.VariableMetadata{}, fmt.Errorf("variable OID %s is not defined", varOID) + return VariableMetadata{}, fmt.Errorf("variable OID %s is not defined", varOID) } return varData, nil @@ -123,7 +117,7 @@ func (or *multiFilesOIDResolver) GetVariableMetadata(trapOID string, varOID stri } recreatedVarOID = varOID[:lastDot] } - return oidresolver.VariableMetadata{}, fmt.Errorf("variable OID %s is not defined", varOID) + return VariableMetadata{}, fmt.Errorf("variable OID %s is not defined", varOID) } func getSortedFileNames(files []fs.DirEntry, logger log.Component) []string { @@ -157,7 +151,7 @@ func getSortedFileNames(files []fs.DirEntry, logger log.Component) []string { return append(ddProvidedFileNames, userProvidedFileNames...) } -func (or *multiFilesOIDResolver) updateFromFile(filePath string) error { +func (or *MultiFilesOIDResolver) updateFromFile(filePath string) error { var fileReader io.ReadCloser fileReader, err := os.Open(filePath) if err != nil { @@ -180,12 +174,12 @@ func (or *multiFilesOIDResolver) updateFromFile(filePath string) error { return or.updateFromReader(fileReader, unmarshalMethod) } -func (or *multiFilesOIDResolver) updateFromReader(reader io.Reader, unmarshalMethod unmarshaller) error { +func (or *MultiFilesOIDResolver) updateFromReader(reader io.Reader, unmarshalMethod unmarshaller) error { fileContent, err := io.ReadAll(reader) if err != nil { return err } - var trapData oidresolver.TrapDBFileContent + var trapData TrapDBFileContent err = unmarshalMethod(fileContent, &trapData) if err != nil { return err @@ -195,16 +189,16 @@ func (or *multiFilesOIDResolver) updateFromReader(reader io.Reader, unmarshalMet return nil } -func (or *multiFilesOIDResolver) updateResolverWithData(trapDB oidresolver.TrapDBFileContent) { - definedVariables := oidresolver.VariableSpec{} +func (or *MultiFilesOIDResolver) updateResolverWithData(trapDB TrapDBFileContent) { + definedVariables := variableSpec{} allOIDs := make([]string, 0, len(trapDB.Variables)) for variableOID := range trapDB.Variables { - if !oidresolver.IsValidOID(variableOID) { + if !IsValidOID(variableOID) { or.logger.Warnf("trap variable OID %s does not look like a valid OID", variableOID) continue } - allOIDs = append(allOIDs, oidresolver.NormalizeOID(variableOID)) + allOIDs = append(allOIDs, NormalizeOID(variableOID)) } // "Fast" algorithm used to mark OID that act both as a variable and as a parent of other variable @@ -223,28 +217,52 @@ func (or *multiFilesOIDResolver) updateResolverWithData(trapDB oidresolver.TrapD } variableData := trapDB.Variables[variableOID] - variableData.IsIntermediateNode = isIntermediateNode + variableData.isIntermediateNode = isIntermediateNode definedVariables[variableOID] = variableData } for _, nodeOID := range nodesOIDThatShouldNeverMatch { - definedVariables[nodeOID] = oidresolver.VariableMetadata{Name: "unknown", IsIntermediateNode: true} + definedVariables[nodeOID] = VariableMetadata{Name: "unknown", isIntermediateNode: true} } for trapOID, trapData := range trapDB.Traps { - if !oidresolver.IsValidOID(trapOID) { + if !IsValidOID(trapOID) { or.logger.Errorf("trap OID %s does not look like a valid OID", trapOID) continue } - trapOID := oidresolver.NormalizeOID(trapOID) + trapOID := NormalizeOID(trapOID) if _, trapConflict := or.traps[trapOID]; trapConflict { or.logger.Debugf("a trap with OID %s is defined in multiple traps db files", trapOID) } - or.traps[trapOID] = oidresolver.TrapMetadata{ + or.traps[trapOID] = TrapMetadata{ Name: trapData.Name, Description: trapData.Description, MIBName: trapData.MIBName, - VariableSpecPtr: definedVariables, + variableSpecPtr: definedVariables, + } + } +} + +// NormalizeOID converts an OID from the absolute form ".1.2.3..." to a relative form "1.2.3..." +func NormalizeOID(value string) string { + // OIDs can be formatted as ".1.2.3..." ("absolute form") or "1.2.3..." ("relative form"). + // Convert everything to relative form, like we do in the Python check. + return strings.TrimLeft(value, ".") +} + +// IsValidOID returns true if value looks like a valid OID. +// An OID is made of digits and dots, but OIDs do not end with a dot and there are always +// digits between dots. +func IsValidOID(value string) bool { + var previousChar rune + for _, char := range value { + if char != '.' && !unicode.IsDigit(char) { + return false + } + if char == '.' && previousChar == '.' { + return false } + previousChar = char } + return previousChar != '.' } diff --git a/comp/snmptraps/oidresolver/oidresolverimpl/oid_resolver_test.go b/pkg/snmp/traps/oid_resolver/oid_resolver_test.go similarity index 75% rename from comp/snmptraps/oidresolver/oidresolverimpl/oid_resolver_test.go rename to pkg/snmp/traps/oid_resolver/oid_resolver_test.go index b999edabf60718..bcbd4cf1da8d63 100644 --- a/comp/snmptraps/oidresolver/oidresolverimpl/oid_resolver_test.go +++ b/pkg/snmp/traps/oid_resolver/oid_resolver_test.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2022-present Datadog, Inc. -package oidresolverimpl +package oidresolver import ( "bytes" @@ -16,7 +16,6 @@ import ( "time" "github.com/DataDog/datadog-agent/comp/core/log" - "github.com/DataDog/datadog-agent/comp/snmptraps/oidresolver" "github.com/DataDog/datadog-agent/pkg/util/fxutil" "github.com/stretchr/testify/require" "gopkg.in/yaml.v2" @@ -43,15 +42,15 @@ func (m MockedDirEntry) Type() fs.FileMode { return 0 } func TestDecoding(t *testing.T) { - trapDBFile := &oidresolver.TrapDBFileContent{ - Traps: oidresolver.TrapSpec{ - "foo": oidresolver.TrapMetadata{ + trapDBFile := &TrapDBFileContent{ + Traps: TrapSpec{ + "foo": TrapMetadata{ Name: "xx", MIBName: "yy", }, }, - Variables: oidresolver.VariableSpec{ - "bar": oidresolver.VariableMetadata{ + Variables: variableSpec{ + "bar": VariableMetadata{ Name: "yy", Description: "dummy description", Enumeration: map[int]string{2: "test"}, @@ -107,11 +106,11 @@ func TestSortFiles(t *testing.T) { func TestResolverWithNonStandardOIDs(t *testing.T) { logger := fxutil.Test[log.Component](t, log.MockModule) - resolver := &multiFilesOIDResolver{traps: make(oidresolver.TrapSpec), logger: logger} - trapData := oidresolver.TrapDBFileContent{ - Traps: oidresolver.TrapSpec{"1.3.6.1.4.1.8072.2.3.0.1": oidresolver.TrapMetadata{Name: "netSnmpExampleHeartbeat", MIBName: "NET-SNMP-EXAMPLES-MIB"}}, - Variables: oidresolver.VariableSpec{ - "1.3.6.1.4.1.8072.2.3.2.1": oidresolver.VariableMetadata{ + resolver := &MultiFilesOIDResolver{traps: make(TrapSpec), logger: logger} + trapData := TrapDBFileContent{ + Traps: TrapSpec{"1.3.6.1.4.1.8072.2.3.0.1": TrapMetadata{Name: "netSnmpExampleHeartbeat", MIBName: "NET-SNMP-EXAMPLES-MIB"}}, + Variables: variableSpec{ + "1.3.6.1.4.1.8072.2.3.2.1": VariableMetadata{ Name: "netSnmpExampleHeartbeatRate", }, }, @@ -136,12 +135,12 @@ func TestResolverWithNonStandardOIDs(t *testing.T) { } func TestResolverWithConflictingTrapOID(t *testing.T) { logger := fxutil.Test[log.Component](t, log.MockModule) - resolver := &multiFilesOIDResolver{traps: make(oidresolver.TrapSpec), logger: logger} - trapDataA := oidresolver.TrapDBFileContent{ - Traps: oidresolver.TrapSpec{"1.3.6.1.4.1.8072.2.3.0.1": oidresolver.TrapMetadata{Name: "foo", MIBName: "FOO-MIB"}}, + resolver := &MultiFilesOIDResolver{traps: make(TrapSpec), logger: logger} + trapDataA := TrapDBFileContent{ + Traps: TrapSpec{"1.3.6.1.4.1.8072.2.3.0.1": TrapMetadata{Name: "foo", MIBName: "FOO-MIB"}}, } - trapDataB := oidresolver.TrapDBFileContent{ - Traps: oidresolver.TrapSpec{"1.3.6.1.4.1.8072.2.3.0.1": oidresolver.TrapMetadata{Name: "bar", MIBName: "BAR-MIB"}}, + trapDataB := TrapDBFileContent{ + Traps: TrapSpec{"1.3.6.1.4.1.8072.2.3.0.1": TrapMetadata{Name: "bar", MIBName: "BAR-MIB"}}, } updateResolverWithIntermediateJSONReader(t, resolver, trapDataA) updateResolverWithIntermediateYAMLReader(t, resolver, trapDataB) @@ -153,19 +152,19 @@ func TestResolverWithConflictingTrapOID(t *testing.T) { func TestResolverWithConflictingVariables(t *testing.T) { logger := fxutil.Test[log.Component](t, log.MockModule) - resolver := &multiFilesOIDResolver{traps: make(oidresolver.TrapSpec), logger: logger} - trapDataA := oidresolver.TrapDBFileContent{ - Traps: oidresolver.TrapSpec{"1.3.6.1.4.1.8072.2.3.0.1": oidresolver.TrapMetadata{}}, - Variables: oidresolver.VariableSpec{ - "1.3.6.1.4.1.8072.2.3.2.1": oidresolver.VariableMetadata{ + resolver := &MultiFilesOIDResolver{traps: make(TrapSpec), logger: logger} + trapDataA := TrapDBFileContent{ + Traps: TrapSpec{"1.3.6.1.4.1.8072.2.3.0.1": TrapMetadata{}}, + Variables: variableSpec{ + "1.3.6.1.4.1.8072.2.3.2.1": VariableMetadata{ Name: "netSnmpExampleHeartbeatRate", }, }, } - trapDataB := oidresolver.TrapDBFileContent{ - Traps: oidresolver.TrapSpec{"1.3.6.1.4.1.8072.2.3.0.2": oidresolver.TrapMetadata{}}, - Variables: oidresolver.VariableSpec{ - "1.3.6.1.4.1.8072.2.3.2.1": oidresolver.VariableMetadata{ + trapDataB := TrapDBFileContent{ + Traps: TrapSpec{"1.3.6.1.4.1.8072.2.3.0.2": TrapMetadata{}}, + Variables: variableSpec{ + "1.3.6.1.4.1.8072.2.3.2.1": VariableMetadata{ Name: "netSnmpExampleHeartbeatRate2", }, }, @@ -184,7 +183,7 @@ func TestResolverWithConflictingVariables(t *testing.T) { func TestResolverWithSuffixedVariable(t *testing.T) { logger := fxutil.Test[log.Component](t, log.MockModule) - resolver := &multiFilesOIDResolver{traps: make(oidresolver.TrapSpec), logger: logger} + resolver := &MultiFilesOIDResolver{traps: make(TrapSpec), logger: logger} updateResolverWithIntermediateJSONReader(t, resolver, dummyTrapDB) data, err := resolver.GetVariableMetadata("1.3.6.1.6.3.1.1.5.4", "1.3.6.1.2.1.2.2.1.1") @@ -206,15 +205,15 @@ func TestResolverWithSuffixedVariable(t *testing.T) { func TestResolverWithSuffixedVariableAndNodeConflict(t *testing.T) { logger := fxutil.Test[log.Component](t, log.MockModule) - resolver := &multiFilesOIDResolver{traps: make(oidresolver.TrapSpec), logger: logger} - trapDB := oidresolver.TrapDBFileContent{ - Traps: oidresolver.TrapSpec{ - "1.3.6.1.6.3.1.1.5.4": oidresolver.TrapMetadata{Name: "linkUp", MIBName: "IF-MIB"}, + resolver := &MultiFilesOIDResolver{traps: make(TrapSpec), logger: logger} + trapDB := TrapDBFileContent{ + Traps: TrapSpec{ + "1.3.6.1.6.3.1.1.5.4": TrapMetadata{Name: "linkUp", MIBName: "IF-MIB"}, }, - Variables: oidresolver.VariableSpec{ - "1.3.6.1.2.1.2.2": oidresolver.VariableMetadata{Name: "NodeConflict"}, - "1.3.6.1.2.1.2.2.1.1": oidresolver.VariableMetadata{Name: "ifIndex"}, - "1.3.6.1.2.1.2.3": oidresolver.VariableMetadata{Name: "NotAConflict"}, + Variables: variableSpec{ + "1.3.6.1.2.1.2.2": VariableMetadata{Name: "NodeConflict"}, + "1.3.6.1.2.1.2.2.1.1": VariableMetadata{Name: "ifIndex"}, + "1.3.6.1.2.1.2.3": VariableMetadata{Name: "NotAConflict"}, }, } updateResolverWithIntermediateJSONReader(t, resolver, trapDB) @@ -244,16 +243,16 @@ func TestResolverWithSuffixedVariableAndNodeConflict(t *testing.T) { func TestResolverWithNoMatchVariableShouldStopBeforeRoot(t *testing.T) { logger := fxutil.Test[log.Component](t, log.MockModule) - resolver := &multiFilesOIDResolver{traps: make(oidresolver.TrapSpec), logger: logger} - trapDB := oidresolver.TrapDBFileContent{ - Traps: oidresolver.TrapSpec{ - "1.3.6.1.6.3.1.1.5.4": oidresolver.TrapMetadata{Name: "linkUp", MIBName: "IF-MIB"}, + resolver := &MultiFilesOIDResolver{traps: make(TrapSpec), logger: logger} + trapDB := TrapDBFileContent{ + Traps: TrapSpec{ + "1.3.6.1.6.3.1.1.5.4": TrapMetadata{Name: "linkUp", MIBName: "IF-MIB"}, }, - Variables: oidresolver.VariableSpec{ - "1": oidresolver.VariableMetadata{Name: "should-not-resolve"}, - "1.3": oidresolver.VariableMetadata{Name: "should-not-resolve"}, - "1.3.6.1": oidresolver.VariableMetadata{Name: "should-not-resolve"}, - "1.3.6.1.4": oidresolver.VariableMetadata{Name: "should-not-resolve"}, + Variables: variableSpec{ + "1": VariableMetadata{Name: "should-not-resolve"}, + "1.3": VariableMetadata{Name: "should-not-resolve"}, + "1.3.6.1": VariableMetadata{Name: "should-not-resolve"}, + "1.3.6.1.4": VariableMetadata{Name: "should-not-resolve"}, }, } updateResolverWithIntermediateJSONReader(t, resolver, trapDB) @@ -274,7 +273,7 @@ func TestResolverWithNoMatchVariableShouldStopBeforeRoot(t *testing.T) { } -func updateResolverWithIntermediateJSONReader(t *testing.T, oidResolver *multiFilesOIDResolver, trapData oidresolver.TrapDBFileContent) { +func updateResolverWithIntermediateJSONReader(t *testing.T, oidResolver *MultiFilesOIDResolver, trapData TrapDBFileContent) { data, err := json.Marshal(trapData) require.NoError(t, err) @@ -283,7 +282,7 @@ func updateResolverWithIntermediateJSONReader(t *testing.T, oidResolver *multiFi require.NoError(t, err) } -func updateResolverWithIntermediateYAMLReader(t *testing.T, oidResolver *multiFilesOIDResolver, trapData oidresolver.TrapDBFileContent) { +func updateResolverWithIntermediateYAMLReader(t *testing.T, oidResolver *MultiFilesOIDResolver, trapData TrapDBFileContent) { data, err := yaml.Marshal(trapData) require.NoError(t, err) @@ -308,7 +307,7 @@ func TestIsValidOID_PropertyBasedTesting(t *testing.T) { recreatedOID = "." + recreatedOID } validOIDs[i] = recreatedOID - require.True(t, oidresolver.IsValidOID(validOIDs[i]), "OID: %s", validOIDs[i]) + require.True(t, IsValidOID(validOIDs[i]), "OID: %s", validOIDs[i]) } var invalidRunes = []rune(",?><|\\}{[]()*&^%$#@!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") @@ -333,7 +332,7 @@ func TestIsValidOID_PropertyBasedTesting(t *testing.T) { oid = strings.Join(oidParts, ".") } - require.False(t, oidresolver.IsValidOID(oid), "OID: %s", oid) + require.False(t, IsValidOID(oid), "OID: %s", oid) } } @@ -350,6 +349,6 @@ func TestIsValidOID_Unit(t *testing.T) { } for oid, expected := range cases { - require.Equal(t, expected, oidresolver.IsValidOID(oid)) + require.Equal(t, expected, IsValidOID(oid)) } } diff --git a/comp/snmptraps/oidresolver/traps_db.go b/pkg/snmp/traps/oid_resolver/traps_db.go similarity index 56% rename from comp/snmptraps/oidresolver/traps_db.go rename to pkg/snmp/traps/oid_resolver/traps_db.go index c6384e2bef8002..716839c0713f32 100644 --- a/comp/snmptraps/oidresolver/traps_db.go +++ b/pkg/snmp/traps/oid_resolver/traps_db.go @@ -5,36 +5,31 @@ package oidresolver -import ( - "strings" - "unicode" -) - // VariableMetadata is the MIB-extracted information of a given trap variable type VariableMetadata struct { Name string `yaml:"name" json:"name"` Description string `yaml:"descr" json:"descr"` Enumeration map[int]string `yaml:"enum" json:"enum"` Bits map[int]string `yaml:"bits" json:"bits"` - IsIntermediateNode bool `yaml:"-" json:"-"` + isIntermediateNode bool // In theory, variables should always be leaves of the OID tree as intermediate nodes do not contain data. // This isn't true in practice (see 1.3.6.1.4.1.4962.2.1.6.3). // Variables are resolved by 'climbing' up the OID tree until finding a match, but variables that are known to be nodes // should never be used for resolving. } -// VariableSpec contains the variableMetadata for each known variable of a given trap db file -type VariableSpec map[string]VariableMetadata +// variableSpec contains the variableMetadata for each known variable of a given trap db file +type variableSpec map[string]VariableMetadata // TrapMetadata is the MIB-extracted information of a given trap OID. // It also contains a reference to the variableSpec that was defined in the same trap db file. // This is to prevent variable conflicts and to give precedence to the variable definitions located] // in the same trap db file as the trap. type TrapMetadata struct { - Name string `yaml:"name" json:"name"` - MIBName string `yaml:"mib" json:"mib"` - Description string `yaml:"descr" json:"descr"` - VariableSpecPtr VariableSpec `yaml:"-" json:"-"` + Name string `yaml:"name" json:"name"` + MIBName string `yaml:"mib" json:"mib"` + Description string `yaml:"descr" json:"descr"` + variableSpecPtr variableSpec } // TrapSpec contains the variableMetadata for each known trap in all trap db files @@ -43,29 +38,5 @@ type TrapSpec map[string]TrapMetadata // TrapDBFileContent contains data for the traps and variables from a trap db file. type TrapDBFileContent struct { Traps TrapSpec `yaml:"traps" json:"traps"` - Variables VariableSpec `yaml:"vars" json:"vars"` -} - -// NormalizeOID converts an OID from the absolute form ".1.2.3..." to a relative form "1.2.3..." -func NormalizeOID(value string) string { - // OIDs can be formatted as ".1.2.3..." ("absolute form") or "1.2.3..." ("relative form"). - // Convert everything to relative form, like we do in the Python check. - return strings.TrimLeft(value, ".") -} - -// IsValidOID returns true if value looks like a valid OID. -// An OID is made of digits and dots, but OIDs do not end with a dot and there are always -// digits between dots. -func IsValidOID(value string) bool { - var previousChar rune - for _, char := range value { - if char != '.' && !unicode.IsDigit(char) { - return false - } - if char == '.' && previousChar == '.' { - return false - } - previousChar = char - } - return previousChar != '.' + Variables variableSpec `yaml:"vars" json:"vars"` } diff --git a/comp/snmptraps/packet/packet.go b/pkg/snmp/traps/packet/packet.go similarity index 100% rename from comp/snmptraps/packet/packet.go rename to pkg/snmp/traps/packet/packet.go diff --git a/comp/snmptraps/packet/packet_test.go b/pkg/snmp/traps/packet/packet_test.go similarity index 100% rename from comp/snmptraps/packet/packet_test.go rename to pkg/snmp/traps/packet/packet_test.go diff --git a/comp/snmptraps/packet/test_helpers.go b/pkg/snmp/traps/packet/test_helpers.go similarity index 100% rename from comp/snmptraps/packet/test_helpers.go rename to pkg/snmp/traps/packet/test_helpers.go diff --git a/pkg/snmp/traps/server.go b/pkg/snmp/traps/server.go new file mode 100644 index 00000000000000..9562bbff947278 --- /dev/null +++ b/pkg/snmp/traps/server.go @@ -0,0 +1,146 @@ +// 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 2020-present Datadog, Inc. + +// Package traps implements a server that listens for SNMP traps and forwards +// useful information to the DD backend. +package traps + +import ( + "fmt" + "time" + + "github.com/DataDog/datadog-agent/comp/core/config" + "github.com/DataDog/datadog-agent/comp/core/log" + "github.com/DataDog/datadog-agent/pkg/aggregator" + "github.com/DataDog/datadog-agent/pkg/aggregator/sender" + trapsconfig "github.com/DataDog/datadog-agent/pkg/snmp/traps/config" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/formatter" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/forwarder" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/listener" + oidresolver "github.com/DataDog/datadog-agent/pkg/snmp/traps/oid_resolver" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/packet" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/status" +) + +// TrapServer manages an SNMP trap listener. +type TrapServer struct { + Addr string + config *trapsconfig.TrapsConfig + listener *listener.TrapListener + sender *forwarder.TrapForwarder + logger log.Component +} + +var ( + serverInstance *TrapServer +) + +// StartServer starts the global trap server. +func StartServer(agentHostname string, demux aggregator.Demultiplexer, conf config.Component, logger log.Component) error { + config, err := trapsconfig.ReadConfig(agentHostname, conf) + if err != nil { + return err + } + sender, err := demux.GetDefaultSender() + if err != nil { + return err + } + oidResolver, err := oidresolver.NewMultiFilesOIDResolver(conf.GetString("confd_path"), logger) + if err != nil { + return err + } + formatter, err := formatter.NewJSONFormatter(oidResolver, sender, logger) + if err != nil { + return err + } + status := status.New() + server, err := NewTrapServer(config, formatter, sender, logger, status) + serverInstance = server + return err +} + +// StopServer stops the global trap server, if it is running. +func StopServer() { + if serverInstance != nil { + serverInstance.Stop() + serverInstance = nil + } +} + +// IsRunning returns whether the trap server is currently running. +func IsRunning() bool { + return serverInstance != nil +} + +// NewTrapServer configures and returns a running SNMP traps server. +func NewTrapServer(config *trapsconfig.TrapsConfig, formatter formatter.Formatter, aggregator sender.Sender, logger log.Component, status status.Manager) (*TrapServer, error) { + packets := make(packet.PacketsChannel, config.GetPacketChannelSize()) + + listener, err := startSNMPTrapListener(config, aggregator, packets, logger, status) + if err != nil { + return nil, err + } + + trapForwarder, err := startSNMPTrapForwarder(formatter, aggregator, packets, logger) + if err != nil { + return nil, fmt.Errorf("unable to start trapForwarder: %w. Will not listen for SNMP traps", err) + } + server := &TrapServer{ + listener: listener, + config: config, + sender: trapForwarder, + logger: logger, + } + + return server, nil +} + +func startSNMPTrapForwarder(formatter formatter.Formatter, aggregator sender.Sender, packets packet.PacketsChannel, logger log.Component) (*forwarder.TrapForwarder, error) { + trapForwarder, err := forwarder.NewTrapForwarder(formatter, aggregator, packets, logger) + if err != nil { + return nil, err + } + trapForwarder.Start() + return trapForwarder, nil +} +func startSNMPTrapListener(c *trapsconfig.TrapsConfig, aggregator sender.Sender, packets packet.PacketsChannel, logger log.Component, status status.Manager) (*listener.TrapListener, error) { + trapListener, err := listener.NewTrapListener(c, aggregator, packets, logger, status) + if err != nil { + return nil, err + } + err = trapListener.Start() + if err != nil { + return nil, err + } + return trapListener, nil +} + +// Stop stops the TrapServer. +func (s *TrapServer) Stop() { + stopped := make(chan interface{}) + + go func() { + s.logger.Infof("Stop listening on %s", s.config.Addr()) + s.listener.Stop() + s.sender.Stop() + close(stopped) + }() + + select { + case <-stopped: + case <-time.After(time.Duration(s.config.StopTimeout) * time.Second): + s.logger.Errorf("Stopping server. Timeout after %d seconds", s.config.StopTimeout) + } +} + +// IsEnabled returns whether SNMP trap collection is enabled in the Agent configuration. +func IsEnabled(conf config.Component) bool { + return conf.GetBool("network_devices.snmp_traps.enabled") +} + +// GetStatus returns key-value data for use in status reporting of the traps server. +func GetStatus() map[string]interface{} { + return status.GetStatus() +} diff --git a/pkg/snmp/traps/server_test.go b/pkg/snmp/traps/server_test.go new file mode 100644 index 00000000000000..ab8cbb97b5f965 --- /dev/null +++ b/pkg/snmp/traps/server_test.go @@ -0,0 +1,45 @@ +// 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 2020-present Datadog, Inc. + +package traps + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/DataDog/datadog-agent/comp/core/log" + "github.com/DataDog/datadog-agent/pkg/aggregator/mocksender" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/config" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/formatter" + "github.com/DataDog/datadog-agent/pkg/snmp/traps/status" + "github.com/DataDog/datadog-agent/pkg/util/fxutil" + + ndmtestutils "github.com/DataDog/datadog-agent/pkg/networkdevice/testutils" +) + +func TestStartFailure(t *testing.T) { + /* + Start two servers with the same config to trigger an "address already in use" error. + */ + logger := fxutil.Test[log.Component](t, log.MockModule) + + freePort, err := ndmtestutils.GetFreePort() + require.NoError(t, err) + config := &config.TrapsConfig{Port: freePort, CommunityStrings: []string{"public"}} + + mockSender := mocksender.NewMockSender("snmp-traps-listener") + mockSender.SetupAcceptAll() + status := status.NewMock() + + sucessServer, err := NewTrapServer(config, &formatter.DummyFormatter{}, mockSender, logger, status) + require.NoError(t, err) + require.NotNil(t, sucessServer) + defer sucessServer.Stop() + + failedServer, err := NewTrapServer(config, &formatter.DummyFormatter{}, mockSender, logger, status) + require.Nil(t, failedServer) + require.Error(t, err) +} diff --git a/comp/snmptraps/snmplog/snmplog.go b/pkg/snmp/traps/snmplog/snmplog.go similarity index 100% rename from comp/snmptraps/snmplog/snmplog.go rename to pkg/snmp/traps/snmplog/snmplog.go diff --git a/comp/snmptraps/status/statusimpl/mock.go b/pkg/snmp/traps/status/mock.go similarity index 66% rename from comp/snmptraps/status/statusimpl/mock.go rename to pkg/snmp/traps/status/mock.go index 2e3482bacb7398..bad6851c3b00e9 100644 --- a/comp/snmptraps/status/statusimpl/mock.go +++ b/pkg/snmp/traps/status/mock.go @@ -3,27 +3,16 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2020-present Datadog, Inc. -package statusimpl +package status -import ( - "sync" +import "sync" - "github.com/DataDog/datadog-agent/comp/snmptraps/status" - "github.com/DataDog/datadog-agent/pkg/util/fxutil" - "go.uber.org/fx" -) - -// MockModule defines a fake Component -var MockModule = fxutil.Component( - fx.Provide(newMock), -) - -// newMock returns a Component that uses plain internal values instead of expvars -func newMock() status.Component { +// NewMock returns a Manager that uses plain internal values instead of expvars +func NewMock() Manager { return &mockManager{} } -// mockManager mocks a manager using plain values (not expvars) +// mockManager mocks a Manager using plain values (not expvars) type mockManager struct { trapsPackets, trapsPacketsAuthErrors int64 lock sync.Mutex diff --git a/comp/snmptraps/status/statusimpl/status.go b/pkg/snmp/traps/status/status.go similarity index 82% rename from comp/snmptraps/status/statusimpl/status.go rename to pkg/snmp/traps/status/status.go index c450ae15c48508..5dcfcf0c397b08 100644 --- a/comp/snmptraps/status/statusimpl/status.go +++ b/pkg/snmp/traps/status/status.go @@ -3,22 +3,14 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2020-present Datadog, Inc. -// Package statusimpl implements the Status component. -package statusimpl +// Package status exposes the expvars we use for status tracking. +package status import ( "encoding/json" "expvar" - "github.com/DataDog/datadog-agent/comp/snmptraps/status" "github.com/DataDog/datadog-agent/pkg/epforwarder" - "github.com/DataDog/datadog-agent/pkg/util/fxutil" - "go.uber.org/fx" -) - -// Module defines the fx options for this component. -var Module = fxutil.Component( - fx.Provide(newManager), ) var ( @@ -32,8 +24,16 @@ func init() { trapsExpvars.Set("PacketsAuthErrors", &trapsPacketsAuthErrors) } -// newManager creates a new component -func newManager() status.Component { +// Manager exposes the expvars we care about +type Manager interface { + AddTrapsPackets(int64) + GetTrapsPackets() int64 + AddTrapsPacketsAuthErrors(int64) + GetTrapsPacketsAuthErrors() int64 +} + +// New creates a new manager +func New() Manager { return &manager{} } diff --git a/pkg/status/render.go b/pkg/status/render.go index e43451a1314760..b58fd4345a3ae0 100644 --- a/pkg/status/render.go +++ b/pkg/status/render.go @@ -16,9 +16,9 @@ import ( "github.com/DataDog/datadog-agent/comp/netflow/server" "github.com/DataDog/datadog-agent/comp/otelcol/otlp" - traps "github.com/DataDog/datadog-agent/comp/snmptraps/config" checkstats "github.com/DataDog/datadog-agent/pkg/collector/check/stats" "github.com/DataDog/datadog-agent/pkg/config" + "github.com/DataDog/datadog-agent/pkg/snmp/traps" "github.com/DataDog/datadog-agent/pkg/util/log" ) diff --git a/pkg/status/status.go b/pkg/status/status.go index 21618b46d5f58b..9f80e1fa0c17dc 100644 --- a/pkg/status/status.go +++ b/pkg/status/status.go @@ -18,7 +18,6 @@ import ( "github.com/DataDog/datadog-agent/cmd/agent/common" hostMetadataUtils "github.com/DataDog/datadog-agent/comp/metadata/host/utils" netflowServer "github.com/DataDog/datadog-agent/comp/netflow/server" - traps "github.com/DataDog/datadog-agent/comp/snmptraps/status/statusimpl" "github.com/DataDog/datadog-agent/pkg/clusteragent/admission" "github.com/DataDog/datadog-agent/pkg/clusteragent/clusterchecks" "github.com/DataDog/datadog-agent/pkg/clusteragent/custommetrics" @@ -31,6 +30,7 @@ import ( "github.com/DataDog/datadog-agent/pkg/config" "github.com/DataDog/datadog-agent/pkg/config/utils" logsStatus "github.com/DataDog/datadog-agent/pkg/logs/status" + "github.com/DataDog/datadog-agent/pkg/snmp/traps" "github.com/DataDog/datadog-agent/pkg/util/containers" "github.com/DataDog/datadog-agent/pkg/util/flavor" httputils "github.com/DataDog/datadog-agent/pkg/util/http"