mirror of
https://github.com/akvorado/akvorado.git
synced 2025-12-11 22:14:02 +01:00
inlet/snmp: add proper support for SNMPv3
This commit is contained in:
@@ -25,7 +25,7 @@ type SubnetMap[V any] struct {
|
||||
// Lookup will search for the most specific subnet matching the
|
||||
// provided IP address and return the value associated with it.
|
||||
func (sm *SubnetMap[V]) Lookup(ip net.IP) (V, bool) {
|
||||
if sm.tree == nil {
|
||||
if sm == nil || sm.tree == nil {
|
||||
var value V
|
||||
return value, false
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func (v Version) String() string {
|
||||
return sarama.KafkaVersion(v).String()
|
||||
}
|
||||
|
||||
// MarshalText turns a Kafka version intro a string
|
||||
// MarshalText turns a Kafka version into a string
|
||||
func (v Version) MarshalText() ([]byte, error) {
|
||||
return []byte(v.String()), nil
|
||||
}
|
||||
|
||||
@@ -276,10 +276,20 @@ continuously the exporters. The following keys are accepted:
|
||||
about to expire or need an update
|
||||
- `cache-persist-file` tells where to store cached data on shutdown and
|
||||
read them back on startup
|
||||
- `communities` is a map from a subnets to the community to use for
|
||||
exporters in the provided subnet. Use `::/0` to set the default
|
||||
- `communities` is a map from a subnets to the SNMPv2 community to use
|
||||
for exporters in the provided subnet. Use `::/0` to set the default
|
||||
value. Alternatively, it also accepts a string to use for all
|
||||
exporters.
|
||||
- `security-parameters` is a map from subnets to the SNMPv3 USM
|
||||
security parameters. Like for `communities`, `::/0` can be used to
|
||||
the set the default value. The security paramaters accepts the
|
||||
following keys: `user-name`, `authentication-protocol` (can be
|
||||
omitted, otherwise `MD5`, `SHA`, `SHA224`, `SHA256`, `SHA384`, and
|
||||
`SHA512` are accepted), `authentication-passphrase` (if the previous
|
||||
value was set), `privacy-protocol` (can be omitted, otherwise `DES`,
|
||||
`AES`, `AES192`, `AES256`, `AES192C`, and `AES256C` are accepted,
|
||||
the later being Cisco-variant), `privacy-passphrase` (if the
|
||||
previous value was set), and `context-name`.
|
||||
- `poller-retries` is the number of retries on unsuccessful SNMP requests.
|
||||
- `poller-timeout` tells how much time should the poller wait for an answer.
|
||||
- `workers` tell how many workers to spawn to handle SNMP polling.
|
||||
@@ -288,6 +298,10 @@ As flows missing interface information are discarded, persisting the
|
||||
cache is useful to quickly be able to handle incoming flows. By
|
||||
default, no persistent cache is configured.
|
||||
|
||||
*Akvorado* will use SNMPv3 if there is a match for the
|
||||
`security-parameters` configuration option. Otherwise, it will use
|
||||
SNMPv2.
|
||||
|
||||
### HTTP
|
||||
|
||||
The builtin HTTP server serves various pages. Its configuration
|
||||
|
||||
@@ -13,6 +13,7 @@ identified with a specific icon:
|
||||
|
||||
## Unreleased
|
||||
|
||||
- ✨ *inlet*: add support for SNMPv3 protocol
|
||||
- 🌱 *inlet*: `inlet.snmp.default-community` is now deprecated
|
||||
|
||||
## 1.5.5 - 2022-08-09
|
||||
|
||||
@@ -6,6 +6,7 @@ package snmp
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gosnmp/gosnmp"
|
||||
@@ -35,8 +36,18 @@ type Configuration struct {
|
||||
|
||||
// Communities is a mapping from exporter IPs to SNMPv2 communities
|
||||
Communities *helpers.SubnetMap[string]
|
||||
// Versions is a mapping from exporter IPs to SNMP versions
|
||||
Versions *helpers.SubnetMap[Version]
|
||||
// SecurityParameters is a mapping from exporter IPs to SNMPv3 security parameters
|
||||
SecurityParameters *helpers.SubnetMap[SecurityParameters]
|
||||
}
|
||||
|
||||
// SecurityParameters describes SNMPv3 USM security parameters.
|
||||
type SecurityParameters struct {
|
||||
UserName string `validate:"required"`
|
||||
AuthenticationProtocol AuthProtocol `validate:"required_with=PrivProtocol"`
|
||||
AuthenticationPassphrase string `validate:"required_with=AuthenticationProtocol"`
|
||||
PrivacyProtocol PrivProtocol
|
||||
PrivacyPassphrase string `validate:"required_with=PrivacyProtocol"`
|
||||
ContextName string
|
||||
}
|
||||
|
||||
// DefaultConfiguration represents the default configuration for the SNMP client.
|
||||
@@ -54,9 +65,7 @@ func DefaultConfiguration() Configuration {
|
||||
Communities: helpers.MustNewSubnetMap(map[string]string{
|
||||
"::/0": "public",
|
||||
}),
|
||||
Versions: helpers.MustNewSubnetMap(map[string]Version{
|
||||
"::/0": Version(gosnmp.Version2c),
|
||||
}),
|
||||
SecurityParameters: helpers.MustNewSubnetMap(map[string]SecurityParameters{}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,36 +115,80 @@ func ConfigurationUnmarshallerHook() mapstructure.DecodeHookFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// Version represents an SNMP version
|
||||
type Version gosnmp.SnmpVersion
|
||||
// AuthProtocol represents a SNMPv3 authentication protocol
|
||||
type AuthProtocol gosnmp.SnmpV3AuthProtocol
|
||||
|
||||
// UnmarshalText parses a SNMP version
|
||||
func (v *Version) UnmarshalText(text []byte) error {
|
||||
switch string(text) {
|
||||
case "1":
|
||||
*v = Version(gosnmp.Version1)
|
||||
case "2", "2c":
|
||||
*v = Version(gosnmp.Version2c)
|
||||
case "3":
|
||||
*v = Version(gosnmp.Version3)
|
||||
// UnmarshalText parses a SNMPv3 authentication protocol
|
||||
func (ap *AuthProtocol) UnmarshalText(text []byte) error {
|
||||
switch strings.ToUpper(string(text)) {
|
||||
case "":
|
||||
*ap = AuthProtocol(gosnmp.NoAuth)
|
||||
case "MD5":
|
||||
*ap = AuthProtocol(gosnmp.MD5)
|
||||
case "SHA":
|
||||
*ap = AuthProtocol(gosnmp.SHA)
|
||||
case "SHA224":
|
||||
*ap = AuthProtocol(gosnmp.SHA224)
|
||||
case "SHA256":
|
||||
*ap = AuthProtocol(gosnmp.SHA256)
|
||||
case "SHA384":
|
||||
*ap = AuthProtocol(gosnmp.SHA384)
|
||||
case "SHA512":
|
||||
*ap = AuthProtocol(gosnmp.SHA512)
|
||||
default:
|
||||
return errors.New("unsupported SNMP version")
|
||||
return errors.New("unknown auth protocol")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// String turns a SNMP version into a string
|
||||
func (v Version) String() string {
|
||||
return gosnmp.SnmpVersion(v).String()
|
||||
// String turns a SNMPv3 authentication protocol to a string
|
||||
func (ap AuthProtocol) String() string {
|
||||
return gosnmp.SnmpV3AuthProtocol(ap).String()
|
||||
}
|
||||
|
||||
// MarshalText turns a Kafka version intro a string
|
||||
func (v Version) MarshalText() ([]byte, error) {
|
||||
return []byte(v.String()), nil
|
||||
// MarshalText turns a SNMPv3 authentication protocol to a string
|
||||
func (ap AuthProtocol) MarshalText() ([]byte, error) {
|
||||
return []byte(ap.String()), nil
|
||||
}
|
||||
|
||||
// PrivProtocol represents a SNMPv3 privacy protocol
|
||||
type PrivProtocol gosnmp.SnmpV3PrivProtocol
|
||||
|
||||
// UnmarshalText parses a SNMPv3 privacy protocol
|
||||
func (ap *PrivProtocol) UnmarshalText(text []byte) error {
|
||||
switch strings.ToUpper(string(text)) {
|
||||
case "":
|
||||
*ap = PrivProtocol(gosnmp.NoPriv)
|
||||
case "DES":
|
||||
*ap = PrivProtocol(gosnmp.DES)
|
||||
case "AES":
|
||||
*ap = PrivProtocol(gosnmp.AES)
|
||||
case "AES192":
|
||||
*ap = PrivProtocol(gosnmp.AES192)
|
||||
case "AES256":
|
||||
*ap = PrivProtocol(gosnmp.AES256)
|
||||
case "AES192C":
|
||||
*ap = PrivProtocol(gosnmp.AES192C)
|
||||
case "AES256C":
|
||||
*ap = PrivProtocol(gosnmp.AES256C)
|
||||
default:
|
||||
return errors.New("unknown priv protocol")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// String turns a SNMPv3 privacy protocol to a string
|
||||
func (ap PrivProtocol) String() string {
|
||||
return gosnmp.SnmpV3PrivProtocol(ap).String()
|
||||
}
|
||||
|
||||
// MarshalText turns a SNMPv3 privacy protocol to a string
|
||||
func (ap PrivProtocol) MarshalText() ([]byte, error) {
|
||||
return []byte(ap.String()), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
helpers.AddMapstructureUnmarshallerHook(ConfigurationUnmarshallerHook())
|
||||
helpers.AddMapstructureUnmarshallerHook(helpers.SubnetMapUnmarshallerHook[string]())
|
||||
helpers.AddMapstructureUnmarshallerHook(helpers.SubnetMapUnmarshallerHook[Version]())
|
||||
helpers.AddMapstructureUnmarshallerHook(helpers.SubnetMapUnmarshallerHook[SecurityParameters]())
|
||||
}
|
||||
|
||||
@@ -108,16 +108,28 @@ func TestConfigurationUnmarshallerHook(t *testing.T) {
|
||||
}),
|
||||
},
|
||||
}, {
|
||||
Description: "SNMP version",
|
||||
Description: "SNMP security parameters",
|
||||
Input: gin.H{
|
||||
"versions": "2c",
|
||||
"security-parameters": gin.H{
|
||||
"user-name": "alfred",
|
||||
"authentication-protocol": "sha",
|
||||
"authentication-passphrase": "hello",
|
||||
"privacy-protocol": "aes",
|
||||
"privacy-passphrase": "bye",
|
||||
},
|
||||
},
|
||||
Output: Configuration{
|
||||
Communities: helpers.MustNewSubnetMap(map[string]string{
|
||||
"::/0": "public",
|
||||
}),
|
||||
Versions: helpers.MustNewSubnetMap(map[string]Version{
|
||||
"::/0": Version(gosnmp.Version2c),
|
||||
SecurityParameters: helpers.MustNewSubnetMap(map[string]SecurityParameters{
|
||||
"::/0": SecurityParameters{
|
||||
UserName: "alfred",
|
||||
AuthenticationProtocol: AuthProtocol(gosnmp.SHA),
|
||||
AuthenticationPassphrase: "hello",
|
||||
PrivacyProtocol: PrivProtocol(gosnmp.AES),
|
||||
PrivacyPassphrase: "bye",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -46,7 +46,7 @@ type pollerConfig struct {
|
||||
Retries int
|
||||
Timeout time.Duration
|
||||
Communities *helpers.SubnetMap[string]
|
||||
Versions *helpers.SubnetMap[Version]
|
||||
SecurityParameters *helpers.SubnetMap[SecurityParameters]
|
||||
}
|
||||
|
||||
// newPoller creates a new SNMP poller.
|
||||
@@ -125,9 +125,6 @@ func (p *realPoller) Poll(ctx context.Context, exporter string, port uint16, ifI
|
||||
Context: ctx,
|
||||
Target: exporter,
|
||||
Port: port,
|
||||
Community: p.config.Communities.LookupOrDefault(exporterIP, "public"),
|
||||
Version: gosnmp.SnmpVersion(
|
||||
p.config.Versions.LookupOrDefault(exporterIP, Version(gosnmp.Version2c))),
|
||||
Retries: p.config.Retries,
|
||||
Timeout: p.config.Timeout,
|
||||
UseUnconnectedUDPSocket: true,
|
||||
@@ -136,6 +133,38 @@ func (p *realPoller) Poll(ctx context.Context, exporter string, port uint16, ifI
|
||||
p.metrics.retries.WithLabelValues(exporter).Inc()
|
||||
},
|
||||
}
|
||||
if securityParameters, ok := p.config.SecurityParameters.Lookup(exporterIP); ok {
|
||||
g.Version = gosnmp.Version3
|
||||
g.SecurityModel = gosnmp.UserSecurityModel
|
||||
usmSecurityParameters := gosnmp.UsmSecurityParameters{
|
||||
UserName: securityParameters.UserName,
|
||||
AuthenticationProtocol: gosnmp.SnmpV3AuthProtocol(securityParameters.AuthenticationProtocol),
|
||||
AuthenticationPassphrase: securityParameters.AuthenticationPassphrase,
|
||||
PrivacyProtocol: gosnmp.SnmpV3PrivProtocol(securityParameters.PrivacyProtocol),
|
||||
PrivacyPassphrase: securityParameters.PrivacyPassphrase,
|
||||
}
|
||||
g.SecurityParameters = &usmSecurityParameters
|
||||
if usmSecurityParameters.AuthenticationProtocol == gosnmp.NoAuth {
|
||||
if usmSecurityParameters.PrivacyProtocol == gosnmp.NoPriv {
|
||||
g.MsgFlags = gosnmp.NoAuthNoPriv
|
||||
} else {
|
||||
// Not possible
|
||||
g.MsgFlags = gosnmp.NoAuthNoPriv
|
||||
}
|
||||
} else {
|
||||
if usmSecurityParameters.PrivacyProtocol == gosnmp.NoPriv {
|
||||
g.MsgFlags = gosnmp.AuthNoPriv
|
||||
} else {
|
||||
g.MsgFlags = gosnmp.AuthPriv
|
||||
}
|
||||
}
|
||||
g.ContextName = securityParameters.ContextName
|
||||
fmt.Printf("WWWWAA %+v\n", g)
|
||||
} else {
|
||||
g.Version = gosnmp.Version2c
|
||||
g.Community = p.config.Communities.LookupOrDefault(exporterIP, "public")
|
||||
}
|
||||
|
||||
if err := g.Connect(); err != nil {
|
||||
p.metrics.failures.WithLabelValues(exporter, "connect").Inc()
|
||||
p.errLogger.Err(err).Str("exporter", exporter).Msg("unable to connect")
|
||||
|
||||
@@ -20,15 +20,69 @@ import (
|
||||
)
|
||||
|
||||
func TestPoller(t *testing.T) {
|
||||
cases := []struct {
|
||||
Description string
|
||||
Skip string
|
||||
Config pollerConfig
|
||||
}{
|
||||
{
|
||||
Description: "SNMPv2",
|
||||
Config: pollerConfig{
|
||||
Retries: 2,
|
||||
Timeout: 100 * time.Millisecond,
|
||||
Communities: helpers.MustNewSubnetMap(map[string]string{
|
||||
"::/0": "private",
|
||||
}),
|
||||
},
|
||||
}, {
|
||||
Description: "SNMPv3",
|
||||
Config: pollerConfig{
|
||||
Retries: 2,
|
||||
Timeout: 100 * time.Millisecond,
|
||||
Communities: helpers.MustNewSubnetMap(map[string]string{
|
||||
"::/0": "public",
|
||||
}),
|
||||
SecurityParameters: helpers.MustNewSubnetMap(map[string]SecurityParameters{
|
||||
"::/0": {
|
||||
UserName: "alfred",
|
||||
AuthenticationProtocol: AuthProtocol(gosnmp.MD5),
|
||||
AuthenticationPassphrase: "hello",
|
||||
PrivacyProtocol: PrivProtocol(gosnmp.AES),
|
||||
PrivacyPassphrase: "bye",
|
||||
ContextName: "private",
|
||||
},
|
||||
}),
|
||||
},
|
||||
}, {
|
||||
Description: "SNMPv3 no priv",
|
||||
Skip: "GoSNMPServer is broken with this configuration",
|
||||
Config: pollerConfig{
|
||||
Retries: 2,
|
||||
Timeout: 100 * time.Millisecond,
|
||||
Communities: helpers.MustNewSubnetMap(map[string]string{
|
||||
"::/0": "public",
|
||||
}),
|
||||
SecurityParameters: helpers.MustNewSubnetMap(map[string]SecurityParameters{
|
||||
"::/0": {
|
||||
UserName: "alfred-nopriv",
|
||||
AuthenticationProtocol: AuthProtocol(gosnmp.MD5),
|
||||
AuthenticationPassphrase: "hello",
|
||||
PrivacyProtocol: PrivProtocol(gosnmp.NoPriv),
|
||||
ContextName: "private",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Description, func(t *testing.T) {
|
||||
if tc.Skip != "" {
|
||||
t.Skip(tc.Skip)
|
||||
}
|
||||
got := []string{}
|
||||
r := reporter.NewMock(t)
|
||||
clock := clock.NewMock()
|
||||
config := pollerConfig{
|
||||
Retries: 2,
|
||||
Timeout: 100 * time.Millisecond,
|
||||
Versions: DefaultConfiguration().Versions,
|
||||
Communities: DefaultConfiguration().Communities,
|
||||
}
|
||||
config := tc.Config
|
||||
p := newPoller(r, config, clock, func(exporterIP, exporterName string, ifIndex uint, iface Interface) {
|
||||
got = append(got, fmt.Sprintf("%s %s %d %s %s %d", exporterIP, exporterName,
|
||||
ifIndex, iface.Name, iface.Description, iface.Speed))
|
||||
@@ -36,9 +90,26 @@ func TestPoller(t *testing.T) {
|
||||
|
||||
// Start a new SNMP server
|
||||
master := GoSNMPServer.MasterAgent{
|
||||
SecurityConfig: GoSNMPServer.SecurityConfig{
|
||||
AuthoritativeEngineBoots: 10,
|
||||
Users: []gosnmp.UsmSecurityParameters{
|
||||
{
|
||||
UserName: "alfred",
|
||||
AuthenticationProtocol: gosnmp.MD5,
|
||||
AuthenticationPassphrase: "hello",
|
||||
PrivacyProtocol: gosnmp.AES,
|
||||
PrivacyPassphrase: "bye",
|
||||
}, {
|
||||
UserName: "alfred-nopriv",
|
||||
AuthenticationProtocol: gosnmp.MD5,
|
||||
AuthenticationPassphrase: "hello",
|
||||
PrivacyProtocol: gosnmp.NoPriv,
|
||||
},
|
||||
},
|
||||
},
|
||||
SubAgents: []*GoSNMPServer.SubAgent{
|
||||
{
|
||||
CommunityIDs: []string{"public"},
|
||||
CommunityIDs: []string{"private"},
|
||||
OIDs: []*GoSNMPServer.PDUValueControlItem{
|
||||
{
|
||||
OID: "1.3.6.1.2.1.1.5.0",
|
||||
@@ -142,5 +213,6 @@ func TestPoller(t *testing.T) {
|
||||
if diff := helpers.Diff(gotMetrics, expectedMetrics); diff != "" {
|
||||
t.Fatalf("Metrics (-got, +want):\n%s", diff)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,8 +80,8 @@ func New(r *reporter.Reporter, configuration Configuration, dependencies Depende
|
||||
poller: newPoller(r, pollerConfig{
|
||||
Retries: configuration.PollerRetries,
|
||||
Timeout: configuration.PollerTimeout,
|
||||
Versions: configuration.Versions,
|
||||
Communities: configuration.Communities,
|
||||
SecurityParameters: configuration.SecurityParameters,
|
||||
}, dependencies.Clock, sc.Put),
|
||||
}
|
||||
c.d.Daemon.Track(&c.t, "inlet/snmp")
|
||||
|
||||
Reference in New Issue
Block a user