Skip to content

Commit

Permalink
[snmp/traps] Update to populate multiple V3 users in gosnmp trap list…
Browse files Browse the repository at this point in the history
…ener (#21635)

* Update config to populate multiple user table

* Update tests for config and listener

* Use fork of gosnmp to support multiple users when reading traps

* Add release note (feature)

* Fix go.sum?

* Lint
  • Loading branch information
zoedt authored Jan 5, 2024
1 parent ff70761 commit e295c87
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 65 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -762,3 +762,6 @@ exclude (
github.com/knadh/koanf/maps v0.1.1
github.com/knadh/koanf/providers/confmap v0.1.0
)

// Temporarily use a fork of gosnmp for multiple-user traps support
replace github.com/gosnmp/gosnmp => github.com/zoedt/gosnmp v0.0.0-20231218153121-83a06ce65d5a
4 changes: 2 additions & 2 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 26 additions & 32 deletions pkg/snmp/traps/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,6 @@ func ReadConfig(host string, conf config.Component) (*TrapsConfig, error) {
// SetDefaults sets all unset values to default values, and returns an error
// if any fields are invalid.
func (c *TrapsConfig) SetDefaults(host string, namespace string) error {
// gosnmp only supports one v3 user at the moment.
if len(c.Users) > 1 {
return errors.New("only one user is currently supported in SNMP Traps Listener configuration")
}

// Set defaults.
if c.Port == 0 {
c.Port = defaultPort
Expand Down Expand Up @@ -124,39 +119,38 @@ func (c *TrapsConfig) BuildSNMPParams(logger log.Component) (*gosnmp.GoSNMP, err
Logger: snmpLogger,
}, nil
}
user := c.Users[0]
authProtocol, err := gosnmplib.GetAuthProtocol(user.AuthProtocol)
if err != nil {
return nil, err
}

privProtocol, err := gosnmplib.GetPrivProtocol(user.PrivProtocol)
if err != nil {
return nil, err
}

msgFlags := gosnmp.NoAuthNoPriv
if user.PrivKey != "" {
msgFlags = gosnmp.AuthPriv
} else if user.AuthKey != "" {
msgFlags = gosnmp.AuthNoPriv
}

return &gosnmp.GoSNMP{
Port: c.Port,
Transport: "udp",
Version: gosnmp.Version3, // Always using version3 for traps, only option that works with all SNMP versions simultaneously
SecurityModel: gosnmp.UserSecurityModel,
MsgFlags: msgFlags,
SecurityParameters: &gosnmp.UsmSecurityParameters{
// Set up user security params table from config
usmTable := gosnmp.NewSnmpV3SecurityParametersTable()
for _, user := range c.Users {
authProtocol, err := gosnmplib.GetAuthProtocol(user.AuthProtocol)
if err != nil {
return nil, err
}
privProtocol, err := gosnmplib.GetPrivProtocol(user.PrivProtocol)
if err != nil {
return nil, err
}
err = usmTable.Add(user.Username, &gosnmp.UsmSecurityParameters{
UserName: user.Username,
AuthoritativeEngineID: c.authoritativeEngineID,
AuthenticationProtocol: authProtocol,
AuthenticationPassphrase: user.AuthKey,
PrivacyProtocol: privProtocol,
PrivacyPassphrase: user.PrivKey,
},
Logger: snmpLogger,
})
if err != nil {
return nil, err
}
}

return &gosnmp.GoSNMP{
Port: c.Port,
Transport: "udp",
Version: gosnmp.Version3, // Always using version3 for traps, only option that works with all SNMP versions simultaneously
SecurityModel: gosnmp.UserSecurityModel,
SecurityParameters: &gosnmp.UsmSecurityParameters{AuthoritativeEngineID: c.authoritativeEngineID},
TrapSecurityParametersTable: usmTable,
Logger: snmpLogger,
}, nil
}

Expand Down
103 changes: 76 additions & 27 deletions pkg/snmp/traps/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,54 @@ var expectedEngineIDs = map[string]string{
"VeryLongHostnameThatIsDifferent": "\x80\xff\xff\xff\xff\xe7\x21\xcc\xd7\x0b\xe1\x60\xc5\x18\xd7\xde\x17\x86\xb0\x7d\x36",
}

var usersV3 = []UserV3{
{
Username: "user",
AuthKey: "password",
AuthProtocol: "MD5",
PrivKey: "password",
PrivProtocol: "AES",
},
{
Username: "user",
AuthKey: "password",
AuthProtocol: "SHA",
PrivKey: "password",
PrivProtocol: "DES",
},
{
Username: "user2",
AuthKey: "password",
AuthProtocol: "MD5",
PrivKey: "password",
PrivProtocol: "AES",
},
}

var usmUsers = []*gosnmp.UsmSecurityParameters{
{
UserName: "user",
AuthenticationProtocol: gosnmp.MD5,
AuthenticationPassphrase: "password",
PrivacyProtocol: gosnmp.AES,
PrivacyPassphrase: "password",
},
{
UserName: "user",
AuthenticationProtocol: gosnmp.SHA,
AuthenticationPassphrase: "password",
PrivacyProtocol: gosnmp.DES,
PrivacyPassphrase: "password",
},
{
UserName: "user2",
AuthenticationProtocol: gosnmp.MD5,
AuthenticationPassphrase: "password",
PrivacyProtocol: gosnmp.AES,
PrivacyPassphrase: "password",
},
}

func makeConfig(t *testing.T, trapConfig TrapsConfig) config.Component {
return makeConfigWithGlobalNamespace(t, trapConfig, "")
}
Expand All @@ -55,16 +103,8 @@ func makeConfigWithGlobalNamespace(t *testing.T, trapConfig TrapsConfig, globalN
func TestFullConfig(t *testing.T) {
logger := fxutil.Test[log.Component](t, logimpl.MockModule())
rootConfig := makeConfig(t, TrapsConfig{
Port: 1234,
Users: []UserV3{
{
Username: "user",
AuthKey: "password",
AuthProtocol: "MD5",
PrivKey: "password",
PrivProtocol: "AES",
},
},
Port: 1234,
Users: usersV3,
BindHost: "127.0.0.1",
CommunityStrings: []string{"public"},
StopTimeout: 12,
Expand All @@ -77,15 +117,7 @@ func TestFullConfig(t *testing.T) {
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)
assert.Equal(t, usersV3, config.Users)

params, err := config.BuildSNMPParams(logger)
assert.NoError(t, err)
Expand All @@ -94,14 +126,31 @@ func TestFullConfig(t *testing.T) {
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)
assert.Equal(t, &gosnmp.UsmSecurityParameters{AuthoritativeEngineID: expectedEngineID}, params.SecurityParameters)

table := gosnmp.NewSnmpV3SecurityParametersTable()
for _, usmUser := range usmUsers {
table.Add(usmUser.UserName, usmUser)
}
var usmConfigTests = []struct {
name string
identifier string
}{
{
"identifier: user has 2 entries",
"user",
},
{
"identifier: user2 has 1 entry",
"user2",
},
}
for _, usmConfigTest := range usmConfigTests {
// Compare the security params after initializing the security keys (happens in the add to table)
expected, _ := table.Get(usmConfigTest.identifier)
actual, _ := params.TrapSecurityParametersTable.Get(usmConfigTest.identifier)
assert.ElementsMatch(t, expected, actual)
}
}

func TestMinimalConfig(t *testing.T) {
Expand Down
95 changes: 93 additions & 2 deletions pkg/snmp/traps/listener/listener_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func TestServerV3(t *testing.T) {
_, trapListener, status := listenerTestSetup(t, config)
defer trapListener.Stop()

sendTestV3Trap(t, config, &gosnmp.UsmSecurityParameters{
sendTestV3Trap(t, config, gosnmp.AuthPriv, &gosnmp.UsmSecurityParameters{
UserName: "user",
AuthoritativeEngineID: "foobarbaz",
AuthenticationPassphrase: "password",
Expand All @@ -118,6 +118,97 @@ func TestServerV3(t *testing.T) {
assertVariables(t, packet)
}

var users = []config.UserV3{
{Username: "user", AuthKey: "password", AuthProtocol: "sha", PrivKey: "password", PrivProtocol: "aes"},
{Username: "user2", AuthKey: "password2", AuthProtocol: "md5", PrivKey: "password", PrivProtocol: "des"},
{Username: "user2", AuthKey: "password2", AuthProtocol: "sha", PrivKey: "password", PrivProtocol: "aes"},
{Username: "user3", AuthKey: "password", AuthProtocol: "sha"},
}

func TestServerV3MultipleCredentials(t *testing.T) {
tests := []struct {
name string
msgFlags gosnmp.SnmpV3MsgFlags
secParams *gosnmp.UsmSecurityParameters
}{
{"user AuthPriv SHA/AES succeeds",
gosnmp.AuthPriv,
&gosnmp.UsmSecurityParameters{
UserName: "user",
AuthoritativeEngineID: "foobarbaz",
AuthenticationPassphrase: "password",
AuthenticationProtocol: gosnmp.SHA,
PrivacyPassphrase: "password",
PrivacyProtocol: gosnmp.AES,
},
},
{"user2 (multiple entries) AuthPriv MD5/DES succeeds",
gosnmp.AuthPriv,
&gosnmp.UsmSecurityParameters{
UserName: "user",
AuthoritativeEngineID: "foobarbaz",
AuthenticationPassphrase: "password",
AuthenticationProtocol: gosnmp.SHA,
PrivacyPassphrase: "password",
PrivacyProtocol: gosnmp.AES,
},
},
{"user2 (multiple entries) AuthPriv SHA/AES succeeds",
gosnmp.AuthPriv,
&gosnmp.UsmSecurityParameters{
UserName: "user",
AuthoritativeEngineID: "foobarbaz",
AuthenticationPassphrase: "password",
AuthenticationProtocol: gosnmp.SHA,
PrivacyPassphrase: "password",
PrivacyProtocol: gosnmp.AES,
},
},
{"user3 AuthNoPriv SHA succeeds",
gosnmp.AuthNoPriv,
&gosnmp.UsmSecurityParameters{
UserName: "user",
AuthoritativeEngineID: "foobarbaz",
AuthenticationPassphrase: "password",
AuthenticationProtocol: gosnmp.SHA,
PrivacyPassphrase: "password",
PrivacyProtocol: gosnmp.AES,
},
},
}
serverPort, err := ndmtestutils.GetFreePort()
require.NoError(t, err)

config := &config.TrapsConfig{Port: serverPort, Users: users}
_, trapListener, status := listenerTestSetup(t, config)
defer trapListener.Stop()

for _, test := range tests {
sendTestV3Trap(t, config, test.msgFlags, test.secParams)
packet, err := receivePacket(t, trapListener, defaultTimeout, status)
require.NoError(t, err)
assertVariables(t, packet)
}
}

func TestServerV3BadCredentialsWithMultipleUsers(t *testing.T) {
serverPort, err := ndmtestutils.GetFreePort()
require.NoError(t, err)
config := &config.TrapsConfig{Port: serverPort, Users: users}
_, trapListener, _ := listenerTestSetup(t, config)
defer trapListener.Stop()

sendTestV3Trap(t, config, gosnmp.AuthPriv, &gosnmp.UsmSecurityParameters{
UserName: "user2",
AuthoritativeEngineID: "foobarbaz",
AuthenticationPassphrase: "password2",
AuthenticationProtocol: gosnmp.SHA,
PrivacyPassphrase: "wrong_password",
PrivacyProtocol: gosnmp.AES,
})
assertNoPacketReceived(t, trapListener)
}

func TestServerV3BadCredentials(t *testing.T) {
serverPort, err := ndmtestutils.GetFreePort()
require.NoError(t, err)
Expand All @@ -126,7 +217,7 @@ func TestServerV3BadCredentials(t *testing.T) {
_, trapListener, _ := listenerTestSetup(t, config)
defer trapListener.Stop()

sendTestV3Trap(t, config, &gosnmp.UsmSecurityParameters{
sendTestV3Trap(t, config, gosnmp.AuthPriv, &gosnmp.UsmSecurityParameters{
UserName: "user",
AuthoritativeEngineID: "foobarbaz",
AuthenticationPassphrase: "password",
Expand Down
4 changes: 2 additions & 2 deletions pkg/snmp/traps/listener/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ func sendTestV2Trap(t *testing.T, trapConfig *config.TrapsConfig, community stri
return params
}

func sendTestV3Trap(t *testing.T, trapConfig *config.TrapsConfig, securityParams *gosnmp.UsmSecurityParameters) *gosnmp.GoSNMP {
func sendTestV3Trap(t *testing.T, trapConfig *config.TrapsConfig, msgFlags gosnmp.SnmpV3MsgFlags, securityParams *gosnmp.UsmSecurityParameters) *gosnmp.GoSNMP {
params, err := trapConfig.BuildSNMPParams(nil)
require.NoError(t, err)
params.MsgFlags = gosnmp.AuthPriv
params.MsgFlags = msgFlags
params.SecurityParameters = securityParams
params.Timeout = 1 * time.Second // Must be non-zero when sending traps.
params.Retries = 1 // Must be non-zero when sending traps.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Each section from every release note are combined when the
# CHANGELOG.rst is rendered. So the text needs to be worded so that
# it does not depend on any information only available in another
# section. This may mean repeating some details, but each section
# must be readable independently of the other.
#
# Each section note must be formatted as reStructuredText.
---
features:
- |
Add support for multiple users when listening for SNMP traps.

0 comments on commit e295c87

Please sign in to comment.