feat(inputs.snmp): Convert octet string with invalid data to hex (#15439)

This commit is contained in:
Thomas Casteleyn 2024-06-04 17:03:26 +02:00 committed by GitHub
parent c2a67ecd03
commit c1bbce3e96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 279 additions and 81 deletions

View File

@ -2,12 +2,14 @@ package snmp
import (
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"math"
"net"
"strconv"
"strings"
"unicode/utf8"
"github.com/gosnmp/gosnmp"
)
@ -94,6 +96,10 @@ func (f *Field) Init(tr Translator) error {
// fieldConvert converts from any type according to the conv specification
func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) {
if f.Conversion == "" {
// OctetStrings may contain hex data that needs its own conversion
if ent.Type == gosnmp.OctetString && !utf8.ValidString(string(ent.Value.([]byte)[:])) {
return hex.EncodeToString(ent.Value.([]byte)), nil
}
if bs, ok := ent.Value.([]byte); ok {
return string(bs), nil
}
@ -188,7 +194,29 @@ func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) {
case []byte:
v = net.HardwareAddr(vt).String()
default:
return nil, fmt.Errorf("invalid type (%T) for hwaddr conversion", v)
return nil, fmt.Errorf("invalid type (%T) for hwaddr conversion", vt)
}
return v, nil
}
if f.Conversion == "hex" {
switch vt := ent.Value.(type) {
case string:
switch ent.Type {
case gosnmp.IPAddress:
ip := net.ParseIP(vt)
if ip4 := ip.To4(); ip4 != nil {
v = hex.EncodeToString(ip4)
} else {
v = hex.EncodeToString(ip)
}
default:
return nil, fmt.Errorf("unsupported Asn1BER (%#v) for hex conversion", ent.Type)
}
case []byte:
v = hex.EncodeToString(vt)
default:
return nil, fmt.Errorf("unsupported type (%T) for hex conversion", vt)
}
return v, nil
}
@ -242,7 +270,7 @@ func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) {
case []byte:
ipbs = vt
default:
return nil, fmt.Errorf("invalid type (%T) for ipaddr conversion", v)
return nil, fmt.Errorf("invalid type (%T) for ipaddr conversion", vt)
}
switch len(ipbs) {

243
internal/snmp/field_test.go Normal file
View File

@ -0,0 +1,243 @@
package snmp
import (
"testing"
"github.com/gosnmp/gosnmp"
"github.com/stretchr/testify/require"
)
func TestConvertDefault(t *testing.T) {
tests := []struct {
name string
ent gosnmp.SnmpPDU
expected interface{}
errmsg string
}{
{
name: "integer",
ent: gosnmp.SnmpPDU{
Type: gosnmp.Integer,
Value: int(2),
},
expected: 2,
},
{
name: "octet string with valid bytes",
ent: gosnmp.SnmpPDU{
Type: gosnmp.OctetString,
Value: []byte{0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64},
},
expected: "Hello world",
},
{
name: "octet string with invalid bytes",
ent: gosnmp.SnmpPDU{
Type: gosnmp.OctetString,
Value: []byte{0x84, 0xc8, 0x7, 0xff, 0xfd, 0x38, 0x54, 0xc1},
},
expected: "84c807fffd3854c1",
},
}
f := Field{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual, err := f.Convert(tt.ent)
if tt.errmsg != "" {
require.ErrorContains(t, err, tt.errmsg)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.expected, actual)
})
}
t.Run("invalid", func(t *testing.T) {
f.Conversion = "invalid"
actual, err := f.Convert(gosnmp.SnmpPDU{})
require.Nil(t, actual)
require.ErrorContains(t, err, "invalid conversion type")
})
}
func TestConvertHex(t *testing.T) {
tests := []struct {
name string
ent gosnmp.SnmpPDU
expected interface{}
errmsg string
}{
{
name: "octet string with valid bytes",
ent: gosnmp.SnmpPDU{
Type: gosnmp.OctetString,
Value: []byte{0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64},
},
expected: "48656c6c6f20776f726c64",
},
{
name: "octet string with invalid bytes",
ent: gosnmp.SnmpPDU{
Type: gosnmp.OctetString,
Value: []byte{0x84, 0xc8, 0x7, 0xff, 0xfd, 0x38, 0x54, 0xc1},
},
expected: "84c807fffd3854c1",
},
{
name: "IPv4",
ent: gosnmp.SnmpPDU{
Type: gosnmp.IPAddress,
Value: "192.0.2.1",
},
expected: "c0000201",
},
{
name: "IPv6",
ent: gosnmp.SnmpPDU{
Type: gosnmp.IPAddress,
Value: "2001:db8::1",
},
expected: "20010db8000000000000000000000001",
},
{
name: "oid",
ent: gosnmp.SnmpPDU{
Type: gosnmp.ObjectIdentifier,
Value: ".1.2.3",
},
errmsg: "unsupported Asn1BER (0x6) for hex conversion",
},
{
name: "integer",
ent: gosnmp.SnmpPDU{
Type: gosnmp.Integer,
Value: int(2),
},
errmsg: "unsupported type (int) for hex conversion",
},
}
f := Field{Conversion: "hex"}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual, err := f.Convert(tt.ent)
if tt.errmsg != "" {
require.ErrorContains(t, err, tt.errmsg)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.expected, actual)
})
}
}
func TestConvertHextoint(t *testing.T) {
tests := []struct {
name string
conversion string
ent gosnmp.SnmpPDU
expected interface{}
errmsg string
}{
{
name: "empty",
conversion: "hextoint:BigEndian:uint64",
ent: gosnmp.SnmpPDU{},
expected: nil,
},
{
name: "big endian uint64",
conversion: "hextoint:BigEndian:uint64",
ent: gosnmp.SnmpPDU{
Type: gosnmp.OctetString,
Value: []byte{0x84, 0xc8, 0x7, 0xff, 0xfd, 0x38, 0x54, 0xc1},
},
expected: uint64(0x84c807fffd3854c1),
},
{
name: "big endian uint32",
conversion: "hextoint:BigEndian:uint32",
ent: gosnmp.SnmpPDU{
Type: gosnmp.OctetString,
Value: []byte{0x84, 0xc8, 0x7, 0xff},
},
expected: uint32(0x84c807ff),
},
{
name: "big endian uint16",
conversion: "hextoint:BigEndian:uint16",
ent: gosnmp.SnmpPDU{
Type: gosnmp.OctetString,
Value: []byte{0x84, 0xc8},
},
expected: uint16(0x84c8),
},
{
name: "big endian invalid",
conversion: "hextoint:BigEndian:invalid",
ent: gosnmp.SnmpPDU{Type: gosnmp.OctetString, Value: []uint8{}},
errmsg: "invalid bit value",
},
{
name: "little endian uint64",
conversion: "hextoint:LittleEndian:uint64",
ent: gosnmp.SnmpPDU{
Type: gosnmp.OctetString,
Value: []byte{0x84, 0xc8, 0x7, 0xff, 0xfd, 0x38, 0x54, 0xc1},
},
expected: uint64(0xc15438fdff07c884),
},
{
name: "little endian uint32",
conversion: "hextoint:LittleEndian:uint32",
ent: gosnmp.SnmpPDU{
Type: gosnmp.OctetString,
Value: []byte{0x84, 0xc8, 0x7, 0xff},
},
expected: uint32(0xff07c884),
},
{
name: "little endian uint16",
conversion: "hextoint:LittleEndian:uint16",
ent: gosnmp.SnmpPDU{
Type: gosnmp.OctetString,
Value: []byte{0x84, 0xc8},
},
expected: uint16(0xc884),
},
{
name: "little endian invalid",
conversion: "hextoint:LittleEndian:invalid",
ent: gosnmp.SnmpPDU{Type: gosnmp.OctetString, Value: []byte{}},
errmsg: "invalid bit value",
},
{
name: "invalid",
conversion: "hextoint:invalid:uint64",
ent: gosnmp.SnmpPDU{Type: gosnmp.OctetString, Value: []byte{}},
errmsg: "invalid Endian value",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := Field{Conversion: tt.conversion}
actual, err := f.Convert(tt.ent)
if tt.errmsg != "" {
require.ErrorContains(t, err, tt.errmsg)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.expected, actual)
})
}
}

View File

@ -294,7 +294,6 @@ func TestFieldConvertGosmi(t *testing.T) {
conv string
expected interface{}
}{
{[]byte("foo"), "", "foo"},
{"0.123", "float", float64(0.123)},
{[]byte("0.123"), "float", float64(0.123)},
{float32(0.123), "float", float64(float32(0.123))},
@ -333,12 +332,6 @@ func TestFieldConvertGosmi(t *testing.T) {
{[]byte("abcd"), "ipaddr", "97.98.99.100"},
{"abcd", "ipaddr", "97.98.99.100"},
{[]byte("abcdefghijklmnop"), "ipaddr", "6162:6364:6566:6768:696a:6b6c:6d6e:6f70"},
{[]byte{0x00, 0x09, 0x3E, 0xE3, 0xF6, 0xD5, 0x3B, 0x60}, "hextoint:BigEndian:uint64", uint64(2602423610063712)},
{[]byte{0x00, 0x09, 0x3E, 0xE3}, "hextoint:BigEndian:uint32", uint32(605923)},
{[]byte{0x00, 0x09}, "hextoint:BigEndian:uint16", uint16(9)},
{[]byte{0x00, 0x09, 0x3E, 0xE3, 0xF6, 0xD5, 0x3B, 0x60}, "hextoint:LittleEndian:uint64", uint64(6934371307618175232)},
{[]byte{0x00, 0x09, 0x3E, 0xE3}, "hextoint:LittleEndian:uint32", uint32(3812493568)},
{[]byte{0x00, 0x09}, "hextoint:LittleEndian:uint16", uint16(2304)},
{3, "enum", "testing"},
{3, "enum(1)", "testing(3)"},
}

View File

@ -4,7 +4,6 @@ package snmp
import (
"testing"
"github.com/gosnmp/gosnmp"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf/testutil"
@ -222,72 +221,6 @@ func TestTableBuild_noWalk(t *testing.T) {
require.Contains(t, tb.Rows, rtr)
}
func TestFieldConvert(t *testing.T) {
testTable := []struct {
input interface{}
conv string
expected interface{}
}{
{[]byte("foo"), "", "foo"},
{"0.123", "float", float64(0.123)},
{[]byte("0.123"), "float", float64(0.123)},
{float32(0.123), "float", float64(float32(0.123))},
{float64(0.123), "float", float64(0.123)},
{float64(0.123123123123), "float", float64(0.123123123123)},
{123, "float", float64(123)},
{123, "float(0)", float64(123)},
{123, "float(4)", float64(0.0123)},
{int8(123), "float(3)", float64(0.123)},
{int16(123), "float(3)", float64(0.123)},
{int32(123), "float(3)", float64(0.123)},
{int64(123), "float(3)", float64(0.123)},
{uint(123), "float(3)", float64(0.123)},
{uint8(123), "float(3)", float64(0.123)},
{uint16(123), "float(3)", float64(0.123)},
{uint32(123), "float(3)", float64(0.123)},
{uint64(123), "float(3)", float64(0.123)},
{"123", "int", int64(123)},
{[]byte("123"), "int", int64(123)},
{"123123123123", "int", int64(123123123123)},
{[]byte("123123123123"), "int", int64(123123123123)},
{float32(12.3), "int", int64(12)},
{float64(12.3), "int", int64(12)},
{int(123), "int", int64(123)},
{int8(123), "int", int64(123)},
{int16(123), "int", int64(123)},
{int32(123), "int", int64(123)},
{int64(123), "int", int64(123)},
{uint(123), "int", int64(123)},
{uint8(123), "int", int64(123)},
{uint16(123), "int", int64(123)},
{uint32(123), "int", int64(123)},
{uint64(123), "int", int64(123)},
{[]byte("abcdef"), "hwaddr", "61:62:63:64:65:66"},
{"abcdef", "hwaddr", "61:62:63:64:65:66"},
{[]byte("abcd"), "ipaddr", "97.98.99.100"},
{"abcd", "ipaddr", "97.98.99.100"},
{[]byte("abcdefghijklmnop"), "ipaddr", "6162:6364:6566:6768:696a:6b6c:6d6e:6f70"},
{[]byte{0x00, 0x09, 0x3E, 0xE3, 0xF6, 0xD5, 0x3B, 0x60}, "hextoint:BigEndian:uint64", uint64(2602423610063712)},
{[]byte{0x00, 0x09, 0x3E, 0xE3}, "hextoint:BigEndian:uint32", uint32(605923)},
{[]byte{0x00, 0x09}, "hextoint:BigEndian:uint16", uint16(9)},
{[]byte{0x00, 0x09, 0x3E, 0xE3, 0xF6, 0xD5, 0x3B, 0x60}, "hextoint:LittleEndian:uint64", uint64(6934371307618175232)},
{[]byte{0x00, 0x09, 0x3E, 0xE3}, "hextoint:LittleEndian:uint32", uint32(3812493568)},
{[]byte{0x00, 0x09}, "hextoint:LittleEndian:uint16", uint16(2304)},
}
for _, tc := range testTable {
f := Field{
Name: "test",
Conversion: tc.conv,
}
require.NoError(t, f.Init(NewNetsnmpTranslator(testutil.Logger{})))
act, err := f.Convert(gosnmp.SnmpPDU{Value: tc.input})
require.NoError(t, err, "input=%T(%v) conv=%s expected=%T(%v)", tc.input, tc.input, tc.conv, tc.expected, tc.expected)
require.EqualValues(t, tc.expected, act, "input=%T(%v) conv=%s expected=%T(%v)", tc.input, tc.input, tc.conv, tc.expected, tc.expected)
}
}
func TestSnmpTranslateCache_miss(t *testing.T) {
snmpTranslateCaches = nil
oid := "IF-MIB::ifPhysAddress.1"

View File

@ -171,11 +171,12 @@ option operate similar to the `snmpget` utility.
## int: Convert the value into an integer.
## hwaddr: Convert the value to a MAC address.
## ipaddr: Convert the value to an IP address.
## hextoint:X:Y Convert a hex string value to integer. Where X is the Endian
## and Y the bit size. For example: hextoint:LittleEndian:uint64
## or hextoint:BigEndian:uint32. Valid options for the Endian are:
## BigEndian and LittleEndian. For the bit size: uint16, uint32
## and uint64.
## hex: Convert bytes to a hex string.
## hextoint:X:Y Convert bytes to integer, where X is the endian and Y the
## bit size. For example: hextoint:LittleEndian:uint64 or
## hextoint:BigEndian:uint32. Valid options for the endian
## are: BigEndian and LittleEndian. For the bit size:
## uint16, uint32 and uint64.
## enum(1): Convert the value according to its syntax in the MIB (full).
## (Only supported with gosmi translator)
## enum: Convert the value according to its syntax in the MIB.