diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c5b6fa7 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +all:gowalk goget trapd +trapd: utils/trapd.go snmp.go ber.go + go build utils/trapd.go +gowalk: utils/gowalk.go snmp.go ber.go + go build utils/gowalk.go +goget: utils/goget.go snmp.go ber.go + go build utils/goget.go + +#Windows Binary +win_trapd: utils/trapd.go snmp.go ber.go + GOOS=windows GOARCH=amd64 go build utils/trapd.go +win_gowalk: utils/gowalk.go snmp.go ber.go + GOOS=windows GOARCH=amd64 go build utils/gowalk.go +win_goget: utils/goget.go snmp.go ber.go + GOOS=windows GOARCH=amd64 go build utils/goget.go + +clean: + rm -f trapd gowalk goget *.exe + diff --git a/README.md b/README.md index bf36dce..c249777 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,31 @@ WapSnmp : SNMP client for golang -------------------------------- +Currently supported operations: +* SNMP v1/v2c/v3 trap receiver with V3 EngineID auto discovery +* SNMP v1/v2c Get, GetMultiple, GetNext, GetBulk, Walk +* SNMP V3 Get, Walk, GetNext -This is an open-source SNMP client library for Go. This allows you to query SNMP servers for any variable, given it's OID (no MIB resolution). - -This library has been written to be in Go style and that means it should be very resistent to all conditions. It's entirely non-blocking/asynchronous and very, very fast. It's also surprisingly small and easy to understand. +Compile +-------------------------------- +make -It supports SNMPv2c or lower (not 3, due to it's complexity), and supports all methods provided as part of that standard. Get, GetMultiple (which are really the same request, but ...), GetNext and GetBulk. +This will compile the following binaries: +* goget : get single SNMP mib using SNMP v3 +* gowalk : walk SNMP mibs using SNMP v3 +* trapd : this program is able to receive SNMP v2 and v3 traps (you need to configure users for SNMP v3 traps) -It has been tested on juniper and cisco devices and has proven to remain stable over long periods of time. +You can run "go test" to perform unit test. -Example: +Using the code +--------------------------------- +* The *_test.go files provide good examples of how to use these functions +* file uder utils/ contain the main entry to the utility program. Then look at example.go and snmp.go to see how it works. - func DoGetTableTest(target string) { - community := "public" - version := SNMPv2c +Not supported yet: +------------------ +* SNMP Informs receiver +* SNMP v3 GetMultiple, GetBulk (these can be easily implemented since SNMP v3 Walk/Get/GetNext is working) - oid := MustParseOid(".1.3.6.1.4.1.2636.3.2.3.1.20") - fmt.Printf("Contacting %v %v %v\n", target, community, version) - wsnmp, err := NewWapSNMP(target, community, version, 2*time.Second, 5) - defer wsnmp.Close() - if err != nil { - fmt.Printf("Error creating wsnmp => %v\n", wsnmp) - return - } - table, err := wsnmp.GetTable(oid) - if err != nil { - fmt.Printf("Error getting table => %v\n", wsnmp) - return - } - for k, v := range table { - fmt.Printf("%v => %v\n", k, v) - } - } -This library can also be used as a ASN1 BER parser. diff --git a/ber.go b/ber.go index af96d67..0b54189 100644 --- a/ber.go +++ b/ber.go @@ -69,7 +69,12 @@ const ( AsnGetRequest BERType = 0xa0 AsnGetNextRequest BERType = 0xa1 AsnGetResponse BERType = 0xa2 + AsnSetRequest BERType = 0xa3 + AsnTrap BERType = 0xa4 AsnGetBulkRequest BERType = 0xa5 + AsnInform BERType = 0xa6 + AsnTrap2 BERType = 0xa7 + AsnReport BERType = 0xa8 ) // Type to indicate which SNMP version is in use. @@ -79,6 +84,7 @@ type SNMPVersion uint8 const ( SNMPv1 SNMPVersion = 0 SNMPv2c SNMPVersion = 1 + SNMPv3 SNMPVersion = 3 ) // EncodeLength encodes an integer value as a BER compliant length value. @@ -139,6 +145,18 @@ func DecodeLength(toparse []byte) (int, int, error) { return val, 1 + numOctets, nil } +func DecodeCounter64(toparse []byte) (uint64, error) { + if len(toparse) > 8 { + return 0, fmt.Errorf("don't support more than 64 bits") + } + var val uint64; + val = 0 + for _, b := range toparse { + val = val*256 + uint64(b) + } + return val, nil +} + /* DecodeInteger decodes an integer. Will error out if it's longer than 64 bits. */ @@ -153,22 +171,32 @@ func DecodeInteger(toparse []byte) (int, error) { return val, nil } +func DecodeIPAddress(toparse []byte) (string, error) { + if len(toparse) != 4 { + return "", fmt.Errorf("need 4 bytes for IP address") + } + return fmt.Sprintf("%d.%d.%d.%d",toparse[0],toparse[1],toparse[2],toparse[3]),nil +} + + // EncodeInteger encodes an integer to BER format. func EncodeInteger(toEncode int) []byte { - // Calculate the length we'll need for the encoded value. - l := 1 + if toEncode==0 { + return []byte{0}; + } + result := make([]byte, 8) + pos := 7 i := toEncode - for i > 255 { + for i > 0{ + result[pos]=byte(i%256); i = i >> 8 - l++ + pos--; } - - // Now create a byte array of the correct length and copy the value into it. - result := make([]byte, l) - for i = 0; i < l; i++ { - result[i] = byte(toEncode >> uint(8*(l-i-1))) + if (result[pos+1]>=0x80){ + result[pos]=0x00; + pos--; } - return result + return result[pos+1:8]; } // DecodeSequence decodes BER binary data into into *[]interface{}. @@ -195,6 +223,7 @@ func DecodeSequence(toparse []byte) ([]interface{}, error) { lidx := 0 idx := 1 + seqLenLen + toparse=toparse[:(1+seqLenLen+seqLength)] // Let's guarantee progress. for idx < len(toparse) && idx > lidx { berType := toparse[idx] @@ -227,25 +256,38 @@ func DecodeSequence(toparse []byte) ([]interface{}, error) { return nil, err } result = append(result, *oid) - case Gauge32: + case Gauge32,Counter32: val, err := DecodeInteger(berValue) if err != nil { return nil, err } result = append(result, val) + case Counter64: + val, err := DecodeCounter64(berValue) + if err != nil { + return nil, err + } + result = append(result, val) + case Timeticks: val, err := DecodeInteger(berValue) if err != nil { return nil, err } result = append(result, time.Duration(val)*10*time.Millisecond) + case Ipaddress: + val, err := DecodeIPAddress(berValue) + if err != nil { + return nil, err + } + result = append(result, val) case Sequence: pdu, err := DecodeSequence(berAll) if err != nil { return nil, err } result = append(result, pdu) - case AsnGetNextRequest, AsnGetRequest, AsnGetResponse: + case AsnGetNextRequest, AsnGetRequest, AsnGetResponse, AsnReport, AsnTrap2, AsnTrap: pdu, err := DecodeSequence(berAll) if err != nil { return nil, err diff --git a/ber_test.go b/ber_test.go index a28ecf8..65d4f2d 100644 --- a/ber_test.go +++ b/ber_test.go @@ -15,11 +15,11 @@ type LengthTest struct { func TestLengthDecodingEncoding(t *testing.T) { tests := []LengthTest{ LengthTest{[]byte{0x26}, 38, 1}, - LengthTest{[]byte{0x81, 0xc9}, 201, 2}, - LengthTest{[]byte{0x81, 0xca}, 202, 2}, - LengthTest{[]byte{0x81, 0x9f}, 159, 2}, - LengthTest{[]byte{0x82, 0x01, 0x70}, 368, 3}, - LengthTest{[]byte{0x81, 0xe3}, 227, 2}, + LengthTest{[]byte{0x82,0x00, 0xc9}, 201, 3}, + LengthTest{[]byte{0x82,0x00, 0xca}, 202, 3}, + LengthTest{[]byte{0x82,0x00, 0x9f}, 159, 3}, + LengthTest{[]byte{0x82,0x01, 0x70}, 368, 3}, + LengthTest{[]byte{0x82,0x00, 0xe3}, 227, 3}, } for _, test := range tests { diff --git a/example.go b/example.go index a07b00e..1baf2cf 100644 --- a/example.go +++ b/example.go @@ -30,31 +30,20 @@ func DoGetTableTest(target string) { } func DoWalkTest(target string) { - community := "public" + community := "monitorhcm" version := SNMPv2c - oid := MustParseOid(".1.3.6.1.2.1") + oid := MustParseOid(".1.3.6.1.2.1.1") + oid0 := oid; fmt.Printf("Contacting %v %v %v\n", target, community, version) wsnmp, err := NewWapSNMP(target, community, version, 2*time.Second, 5) - defer wsnmp.Close() if err != nil { fmt.Printf("Error creating wsnmp => %v\n", wsnmp) return } defer wsnmp.Close() for { - results, err := wsnmp.GetBulk(oid, 50) - if err != nil { - fmt.Printf("GetBulk error => %v\n", err) - return - } - for o, v := range results { - fmt.Printf("%v => %v\n", o, v) - - oid = MustParseOid(o) - } - /* Old version without GETBULK result_oid, val, err := wsnmp.GetNext(oid) if err != nil { fmt.Printf("GetNext error => %v\n", err) @@ -62,7 +51,36 @@ func DoWalkTest(target string) { } fmt.Printf("GetNext(%v, %v, %v, %v) => %s, %v\n", target, community, version, oid, result_oid, val) oid = *result_oid - */ + if ! oid.Within(oid0) { + break; + } + } +} + +func DoWalkTestV3(target string, oidstr,username, authAlg, authKey, privAlg, privKey string) { + oid := MustParseOid(oidstr) + oid0 := oid; + + fmt.Printf("Contacting %v using SNMP v3\n", target) + wsnmp, err := NewWapSNMPv3(target, username, authAlg, authKey, privAlg, privKey, 2*time.Second, 2) + if err != nil { + fmt.Printf("Error creating wsnmp => %v\n", wsnmp) + return + } + defer wsnmp.Close() + wsnmp.Discover(); + for { + result_oid, val, err := wsnmp.GetNextV3(oid) + if err != nil { + fmt.Printf("GetNext error => %v\n", err) + return + } + fmt.Printf("GetNext(%v, %v) => %s, %v\n", target, oid, result_oid, val) + + oid = *result_oid + if ! oid.Within(oid0) { + break; + } } } @@ -93,3 +111,24 @@ func DoGetTest(target string) { fmt.Printf("Get(%v, %v, %v, %v) => %v\n", target, community, version, oid, val) } } + +func DoGetTestV3(target string, oidstr,username, authAlg, authKey, privAlg, privKey string) { + oid := MustParseOid(oidstr) + + fmt.Printf("Contacting %v using SNMP v3\n", target) + wsnmp, err := NewWapSNMPv3(target, username, authAlg, authKey, privAlg, privKey, 2*time.Second, 2) + if err != nil { + fmt.Printf("Error creating wsnmp => %v\n", wsnmp) + return + } + defer wsnmp.Close() + wsnmp.Discover(); + + val, err := wsnmp.GetV3(oid) + if err != nil { + fmt.Printf("GetV3 error => %v\n", err) + return + } + fmt.Printf("GetV3(%v) => %v\n", oid , val) +} + diff --git a/snmp.go b/snmp.go index 2fd296a..ffb5f00 100644 --- a/snmp.go +++ b/snmp.go @@ -6,10 +6,28 @@ import ( "log" "math/rand" "net" - "reflect" "time" + "io" + "crypto/md5" + "crypto/sha1" + "crypto/aes" + "crypto/des" + "crypto/cipher" + "reflect" + "strings" + "bytes" + "encoding/binary" + "errors" ) +type V3user struct { + User string + AuthAlg string //MD5 or SHA1 + AuthPwd string + PrivAlg string //AES or DES + PrivPwd string +} + // The object type that lets you do SNMP requests. type WapSNMP struct { Target string // Target device for these SNMP events. @@ -18,12 +36,64 @@ type WapSNMP struct { timeout time.Duration // Timeout to use for all SNMP packets. retries int // Number of times to retry an operation. conn net.Conn // Cache the UDP connection in the object. + //SNMP V3 variables + user string + authAlg string //MD5 or SHA1 + authPwd string + privAlg string //AES or DES + privPwd string + engineID string + //V3 temp variables + authKey string + privKey string + engineBoots int32 + engineTime int32 + desIV uint32 + aesIV int64 + Trapusers []V3user } const ( bufSize int = 16384 + maxMsgSize int = 65500 + SNMP_AES string = "AES" + SNMP_DES string = "DES" + SNMP_SHA1 string = "SHA1" + SNMP_MD5 string = "MD5" ) + +func password_to_key( password string, engineID string, hash_alg string) string{ + h := sha1.New() + if hash_alg=="MD5" { + h = md5.New() + } + + count := 0; + plen:=len(password); + repeat := 1048576/plen; + remain := 1048576%plen; + for count < repeat { + io.WriteString(h,password); + count++; + } + if remain > 0 { + io.WriteString(h,string(password[:remain])); + } + ku := string(h.Sum(nil)) + //fmt.Printf("ku=% x\n", ku) + + h.Reset(); + io.WriteString(h,ku); + io.WriteString(h,engineID); + io.WriteString(h,ku); + localKey:=h.Sum(nil); + //fmt.Printf("localKey=% x\n", localKey) + + return string(localKey); +} + + // NewWapSNMP creates a new WapSNMP object. Opens a udp connection to the device that will be used for the SNMP packets. func NewWapSNMP(target, community string, version SNMPVersion, timeout time.Duration, retries int) (*WapSNMP, error) { targetPort := fmt.Sprintf("%s:161", target) @@ -31,14 +101,55 @@ func NewWapSNMP(target, community string, version SNMPVersion, timeout time.Dura if err != nil { return nil, fmt.Errorf(`error connecting to ("udp", "%s") : %s`, targetPort, err) } - return &WapSNMP{target, community, version, timeout, retries, conn}, nil + return &WapSNMP{ + Target:target, + Community: community, + Version: version, + timeout: timeout, + retries: retries, + conn: conn, + }, nil +} + +func NewWapSNMPv3(target, user, authAlg, authPwd, privAlg, privPwd string, timeout time.Duration, retries int) (*WapSNMP, error) { + if authAlg != SNMP_MD5 && authAlg != SNMP_SHA1 { + return nil, fmt.Errorf(`Invalid auth algorithm %s, needs SHA1 or MD5`, authAlg) + } + if privAlg != SNMP_AES && privAlg != SNMP_DES { + return nil, fmt.Errorf(`Invalid priv algorithm %s, needs AES or DES`, privAlg) + } + + targetPort := fmt.Sprintf("%s:161", target) + conn, err := net.DialTimeout("udp", targetPort, timeout) + if err != nil { + return nil, fmt.Errorf(`error connecting to ("udp", "%s") : %s`, targetPort, err) + } + return &WapSNMP{ + Target:target, + Version: SNMPv3, + timeout: timeout, + retries: retries, + conn: conn, + user: user, + authAlg: authAlg, + authPwd: authPwd, + privAlg: privAlg, + privPwd: privPwd, + }, nil } /* NewWapSNMPOnConn creates a new WapSNMP object from an existing net.Conn. It does not check if the provided target is valid.*/ func NewWapSNMPOnConn(target, community string, version SNMPVersion, timeout time.Duration, retries int, conn net.Conn) *WapSNMP { - return &WapSNMP{target, community, version, timeout, retries, conn} + return &WapSNMP{ + Target:target, + Community: community, + Version: version, + timeout: timeout, + retries: retries, + conn: conn, + } } // Generate a valid SNMP request ID. @@ -148,6 +259,304 @@ func (w WapSNMP) GetMultiple(oids []Oid) (map[string]interface{}, error) { return result, nil } +/* SNMP V3 requires a discover packet being sent before a request being sent, + so that agent's engineID and other parameters can be automatically detected +*/ +func (w *WapSNMP) Discover() (error) { + msgID := getRandomRequestID() + requestID := getRandomRequestID() + v3Header, _:= EncodeSequence([]interface{}{Sequence,"",0,0,"","",""}) + flags:=string([]byte{4}); + USM := 0x03; + req, err := EncodeSequence([]interface{}{ + Sequence, int(w.Version), + []interface{}{Sequence, msgID, maxMsgSize, flags, USM}, + string(v3Header), + []interface{}{Sequence, "", "", + []interface{}{AsnGetRequest, requestID, 0, 0, []interface{}{Sequence} }}}) + if err != nil { + fmt.Printf("Error encoding in discover:%v\n",err); + panic(err); + } + + response := make([]byte, bufSize) + numRead, err := poll(w.conn, req, response, w.retries, 500*time.Millisecond) + if err != nil { + return err + } + + decodedResponse, err := DecodeSequence(response[:numRead]) + if err != nil { + fmt.Printf("Error decoding discover:%v\n",err); + panic(err); + } + + v3HeaderStr := decodedResponse[3].(string); + v3HeaderDecoded, err := DecodeSequence([]byte(v3HeaderStr)) + if err != nil { + fmt.Printf("Error 2 decoding:%v\n",err); + return err + } + + w.engineID=v3HeaderDecoded[1].(string); + w.engineBoots=int32(v3HeaderDecoded[2].(int)); + w.engineTime=int32(v3HeaderDecoded[3].(int)); + w.aesIV=rand.Int63(); + w.desIV=rand.Uint32(); + //keys + w.authKey = password_to_key(w.authPwd, w.engineID , w.authAlg); + privKey := password_to_key(w.privPwd, w.engineID , w.authAlg); + w.privKey = string(([]byte(privKey))[0:16]) + return nil +} + +func EncryptDESCBC(dst, src, key, iv []byte) error { + desBlockEncrypter, err := des.NewCipher([]byte(key)) + if err != nil { + return err + } + desEncrypter := cipher.NewCBCEncrypter(desBlockEncrypter, iv) + desEncrypter.CryptBlocks(dst, src) + return nil +} + +func DecryptDESCBC(dst, src, key, iv []byte) error { + desBlockEncrypter, err := des.NewCipher([]byte(key)) + if err != nil { + return err + } + desDecrypter := cipher.NewCBCDecrypter(desBlockEncrypter, iv) + desDecrypter.CryptBlocks(dst, src) + return nil +} + + +func EncryptAESCFB(dst, src, key, iv []byte) error { + aesBlockEncrypter, err := aes.NewCipher([]byte(key)) + if err != nil { + return err + } + aesEncrypter := cipher.NewCFBEncrypter(aesBlockEncrypter, iv) + aesEncrypter.XORKeyStream(dst, src) + return nil +} + +func DecryptAESCFB(dst, src, key, iv []byte) error { + aesBlockDecrypter, err := aes.NewCipher([]byte(key)) + if err != nil { + return nil + } + aesDecrypter := cipher.NewCFBDecrypter(aesBlockDecrypter, iv) + aesDecrypter.XORKeyStream(dst, src) + return nil +} + + +func strXor(s1,s2 string) (string) { + if len(s1) != len(s2){ + panic("strXor called with two strings of different length\n"); + } + n := len(s1); + b := make([]byte, n); + for i:=0; i %v\n", err) return diff --git a/utils/goget.go b/utils/goget.go new file mode 100644 index 0000000..fb557bb --- /dev/null +++ b/utils/goget.go @@ -0,0 +1,8 @@ +package main +import ( + snmp "github.com/tiebingzhang/WapSNMP" +) + +func main(){ + snmp.DoGetTestV3("10.0.217.204","1.3.6.1.4.1.27822.8.1.1.6.0", "pcb.snmpv3","SHA1","this_is_my_pcb","AES", "my_pcb_is_4_me"); +} diff --git a/utils/gowalk.go b/utils/gowalk.go new file mode 100644 index 0000000..464d625 --- /dev/null +++ b/utils/gowalk.go @@ -0,0 +1,10 @@ +package main +import ( + //snmp "github.com/cdevr/WapSNMP" + snmp "github.com/tiebingzhang/WapSNMP" +) + +func main(){ + //snmp.DoWalkTest("127.0.0.1"); + snmp.DoWalkTestV3("10.0.217.204","1.3.6.1.4.1.27822.8.1.1", "pcb.snmpv3","SHA1","this_is_my_pcb","AES", "my_pcb_is_4_me"); +} diff --git a/utils/trapd.go b/utils/trapd.go new file mode 100644 index 0000000..5b9720a --- /dev/null +++ b/utils/trapd.go @@ -0,0 +1,54 @@ + +package main + +import ( + snmp "github.com/tiebingzhang/WapSNMP" + "log" + "net" + "math/rand" + "time" +) + +func myUDPServer(listenIPAddr string, port int) *net.UDPConn { + addr := net.UDPAddr{ + Port: port, + IP: net.ParseIP(listenIPAddr), + } + conn, err := net.ListenUDP("udp", &addr) + if err != nil { + log.Printf("udp Listen error."); + panic(err) + } + return conn; +} + +func main() { + rand.Seed(0) + target := "" + community := "" + version := snmp.SNMPv2c + + udpsock := myUDPServer("0.0.0.0",162); + + wsnmp := snmp.NewWapSNMPOnConn(target, community, version, 2*time.Second, 5, udpsock) + defer wsnmp.Close() + + wsnmp.Trapusers = append(wsnmp.Trapusers,snmp.V3user{ "pcb.snmpv3","SHA1","this_is_my_pcb","AES","my_pcb_is_4_me" }); + + packet:=make([]byte,3000); + for { + _,addr,err:=udpsock.ReadFromUDP(packet); + if err!=nil{ + log.Fatal("udp read error\n"); + } + + log.Printf("Received trap from %s:\n",addr.IP); + + err = wsnmp.ParseTrap(packet) + if err != nil { + log.Printf("Error processing trap: %v.", err) + } + } + udpsock.Close(); + +}