From 771cc6ff48cc0315b8b31aca88145f7662ef6aa7 Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Mon, 9 Jun 2025 17:14:28 +0200 Subject: [PATCH] Temporarily add go-agentx (w/ fixes to lexico ordering) --- go-agentx/.gitignore | 4 + go-agentx/AUTHORS | 12 ++ go-agentx/LICENSE | 191 +++++++++++++++++++++++++ go-agentx/README.md | 110 +++++++++++++++ go-agentx/client.go | 217 +++++++++++++++++++++++++++++ go-agentx/environment_test.go | 52 +++++++ go-agentx/go.mod | 5 + go-agentx/go.sum | 12 ++ go-agentx/handler.go | 17 +++ go-agentx/helper_test.go | 31 +++++ go-agentx/list_handler.go | 66 +++++++++ go-agentx/list_handler_test.go | 63 +++++++++ go-agentx/list_item.go | 13 ++ go-agentx/marshaler/multi.go | 33 +++++ go-agentx/pdu/allocate_index.go | 29 ++++ go-agentx/pdu/close.go | 26 ++++ go-agentx/pdu/deallocate_index.go | 29 ++++ go-agentx/pdu/error.go | 62 +++++++++ go-agentx/pdu/flags.go | 45 ++++++ go-agentx/pdu/get.go | 40 ++++++ go-agentx/pdu/get_next.go | 28 ++++ go-agentx/pdu/header.go | 61 ++++++++ go-agentx/pdu/header_packet.go | 38 +++++ go-agentx/pdu/object_identifier.go | 96 +++++++++++++ go-agentx/pdu/octet_string.go | 43 ++++++ go-agentx/pdu/open.go | 38 +++++ go-agentx/pdu/packet.go | 14 ++ go-agentx/pdu/range.go | 53 +++++++ go-agentx/pdu/ranges.go | 27 ++++ go-agentx/pdu/reason.go | 38 +++++ go-agentx/pdu/register.go | 37 +++++ go-agentx/pdu/response.go | 68 +++++++++ go-agentx/pdu/timeout.go | 29 ++++ go-agentx/pdu/type.go | 77 ++++++++++ go-agentx/pdu/unregister.go | 37 +++++ go-agentx/pdu/variable.go | 185 ++++++++++++++++++++++++ go-agentx/pdu/variable_type.go | 59 ++++++++ go-agentx/pdu/variables.go | 56 ++++++++ go-agentx/request.go | 16 +++ go-agentx/session.go | 185 ++++++++++++++++++++++++ go-agentx/session_test.go | 48 +++++++ go-agentx/shell.nix | 7 + go-agentx/snmpd.conf | 7 + go-agentx/value/oid.go | 103 ++++++++++++++ go-agentx/value/oid_test.go | 70 ++++++++++ 45 files changed, 2477 insertions(+) create mode 100644 go-agentx/.gitignore create mode 100644 go-agentx/AUTHORS create mode 100644 go-agentx/LICENSE create mode 100644 go-agentx/README.md create mode 100644 go-agentx/client.go create mode 100644 go-agentx/environment_test.go create mode 100644 go-agentx/go.mod create mode 100644 go-agentx/go.sum create mode 100644 go-agentx/handler.go create mode 100644 go-agentx/helper_test.go create mode 100644 go-agentx/list_handler.go create mode 100644 go-agentx/list_handler_test.go create mode 100644 go-agentx/list_item.go create mode 100644 go-agentx/marshaler/multi.go create mode 100644 go-agentx/pdu/allocate_index.go create mode 100644 go-agentx/pdu/close.go create mode 100644 go-agentx/pdu/deallocate_index.go create mode 100644 go-agentx/pdu/error.go create mode 100644 go-agentx/pdu/flags.go create mode 100644 go-agentx/pdu/get.go create mode 100644 go-agentx/pdu/get_next.go create mode 100644 go-agentx/pdu/header.go create mode 100644 go-agentx/pdu/header_packet.go create mode 100644 go-agentx/pdu/object_identifier.go create mode 100644 go-agentx/pdu/octet_string.go create mode 100644 go-agentx/pdu/open.go create mode 100644 go-agentx/pdu/packet.go create mode 100644 go-agentx/pdu/range.go create mode 100644 go-agentx/pdu/ranges.go create mode 100644 go-agentx/pdu/reason.go create mode 100644 go-agentx/pdu/register.go create mode 100644 go-agentx/pdu/response.go create mode 100644 go-agentx/pdu/timeout.go create mode 100644 go-agentx/pdu/type.go create mode 100644 go-agentx/pdu/unregister.go create mode 100644 go-agentx/pdu/variable.go create mode 100644 go-agentx/pdu/variable_type.go create mode 100644 go-agentx/pdu/variables.go create mode 100644 go-agentx/request.go create mode 100644 go-agentx/session.go create mode 100644 go-agentx/session_test.go create mode 100644 go-agentx/shell.nix create mode 100644 go-agentx/snmpd.conf create mode 100644 go-agentx/value/oid.go create mode 100644 go-agentx/value/oid_test.go diff --git a/go-agentx/.gitignore b/go-agentx/.gitignore new file mode 100644 index 0000000..48ca945 --- /dev/null +++ b/go-agentx/.gitignore @@ -0,0 +1,4 @@ +agentx +Makefile +*.txt +vendor diff --git a/go-agentx/AUTHORS b/go-agentx/AUTHORS new file mode 100644 index 0000000..04dba09 --- /dev/null +++ b/go-agentx/AUTHORS @@ -0,0 +1,12 @@ +# This is the official list of agentx authors for copyright purposes. +# This file is distinct from the CONTRIBUTORS files. +# See the latter for an explanation. + +# Names should be added to this file as +# Name or Organization +# The email address is not required for organizations. + +# Please keep the list sorted. + +Philipp Brüll +Posteo e.K. diff --git a/go-agentx/LICENSE b/go-agentx/LICENSE new file mode 100644 index 0000000..ade9307 --- /dev/null +++ b/go-agentx/LICENSE @@ -0,0 +1,191 @@ +All files in this repository are licensed as follows. If you contribute +to this repository, it is assumed that you license your contribution +under the same license unless you state otherwise. + +All files Copyright (C) 2015 Canonical Ltd. unless otherwise specified in the file. + +This software is licensed under the LGPLv3, included below. + +As a special exception to the GNU Lesser General Public License version 3 +("LGPL3"), the copyright holders of this Library give you permission to +convey to a third party a Combined Work that links statically or dynamically +to this Library without providing any Minimal Corresponding Source or +Minimal Application Code as set out in 4d or providing the installation +information set out in section 4e, provided that you comply with the other +provisions of LGPL3 and provided that you meet, for the Application the +terms and conditions of the license(s) which apply to the Application. + +Except as stated in this special exception, the provisions of LGPL3 will +continue to comply in full to this Library. If you modify this Library, you +may apply this exception to your version of this Library, but you are not +obliged to do so. If you do not wish to do so, delete this exception +statement from your version. This exception does not (and cannot) modify any +license terms which apply to the Application, with which you must still +comply. + + + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/go-agentx/README.md b/go-agentx/README.md new file mode 100644 index 0000000..5e3c620 --- /dev/null +++ b/go-agentx/README.md @@ -0,0 +1,110 @@ +# AgentX + +[![Documentation](https://godoc.org/github.com/posteo/go-agentx?status.svg)](http://godoc.org/github.com/posteo/go-agentx) + +A library with a pure Go implementation of the [AgentX-Protocol](http://tools.ietf.org/html/rfc2741). The library is not yet feature-complete, but should be far enough to used in a production environment. + +The AgentX-Protocol can be used to extend a snmp-daemon such that it dispatches the requests to an OID-subtree to your Go application. Those requests are than handled by this library and can be replied with metrics about your applications state. + +## State + +The library implements all variable types (Integer, OctetString, Null, ObjectIdentifier, IPAddress, Counter32, Gauge32, TimeTicks, Opaque, Counter64, NoSuchObject, NoSuchInstance, EndOfMIBView), but only some of the requests (Get, GetNext, GetBulk). Set-requests and Traps are not implemented yet. + +## Helper + +In order to provided metrics, your have to implement the `agentx.Handler` interface. For convenience, you can use the `agentx.ListHandler` implementation, which takes a list of OIDs and values and serves them if requested. An example is listed below. + +## Example + +```go +package main + +import ( + "log" + "net" + "time" + + "github.com/posteo/go-agentx" + "github.com/posteo/go-agentx/pdu" + "github.com/posteo/go-agentx/value" +) + +func main() { + client, err := agentx.Dial("tcp", "localhost:705") + if err != nil { + log.Fatalf(err) + } + client.Timeout = 1 * time.Minute + client.ReconnectInterval = 1 * time.Second + + session, err := client.Session() + if err != nil { + log.Fatalf(err) + } + + listHandler := &agentx.ListHandler{} + + item := listHandler.Add("1.3.6.1.4.1.45995.3.1") + item.Type = pdu.VariableTypeInteger + item.Value = int32(-123) + + item = listHandler.Add("1.3.6.1.4.1.45995.3.2") + item.Type = pdu.VariableTypeOctetString + item.Value = "echo test" + + item = listHandler.Add("1.3.6.1.4.1.45995.3.3") + item.Type = pdu.VariableTypeNull + item.Value = nil + + item = listHandler.Add("1.3.6.1.4.1.45995.3.4") + item.Type = pdu.VariableTypeObjectIdentifier + item.Value = "1.3.6.1.4.1.45995.1.5" + + item = listHandler.Add("1.3.6.1.4.1.45995.3.5") + item.Type = pdu.VariableTypeIPAddress + item.Value = net.IP{10, 10, 10, 10} + + item = listHandler.Add("1.3.6.1.4.1.45995.3.6") + item.Type = pdu.VariableTypeCounter32 + item.Value = uint32(123) + + item = listHandler.Add("1.3.6.1.4.1.45995.3.7") + item.Type = pdu.VariableTypeGauge32 + item.Value = uint32(123) + + item = listHandler.Add("1.3.6.1.4.1.45995.3.8") + item.Type = pdu.VariableTypeTimeTicks + item.Value = 123 * time.Second + + item = listHandler.Add("1.3.6.1.4.1.45995.3.9") + item.Type = pdu.VariableTypeOpaque + item.Value = []byte{1, 2, 3} + + item = listHandler.Add("1.3.6.1.4.1.45995.3.10") + item.Type = pdu.VariableTypeCounter64 + item.Value = uint64(12345678901234567890) + + session.Handler = listHandler + + if err := session.Register(127, value.MustParseOID("1.3.6.1.4.1.45995.3")); err != nil { + log.Fatalf(err) + } + + for { + time.Sleep(100 * time.Millisecond) + } +} +``` + +## Connection lost + +If the connection to the snmp-daemon is lost, the client tries to reconnect. Therefor the property `ReconnectInterval` has be set. It specifies a duration that is waited before a re-connect is tried. +If the client has open session or registrations, the client try to re-establish both on a successful re-connect. + +## Project + +The implementation was provided by [simia.tech (haftungsbeschränkt)](https://simia.tech). + +## License + +The project is licensed under LGPL 3.0 (see LICENSE file). diff --git a/go-agentx/client.go b/go-agentx/client.go new file mode 100644 index 0000000..3808165 --- /dev/null +++ b/go-agentx/client.go @@ -0,0 +1,217 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package agentx + +import ( + "bufio" + "fmt" + "io" + "log" + "net" + "strings" + "time" + + "github.com/posteo/go-agentx/pdu" + "github.com/posteo/go-agentx/value" +) + +// Client defines an agentx client. +type Client struct { + Timeout time.Duration + ReconnectInterval time.Duration + NameOID value.OID + Name string + + network string + address string + conn net.Conn + requestChan chan *request + sessions map[uint32]*Session +} + +// Dial connects to the provided agentX endpoint. +func Dial(network, address string) (*Client, error) { + conn, err := net.Dial(network, address) + if err != nil { + return nil, fmt.Errorf("dial %s %s: %w", network, address, err) + } + c := &Client{ + network: network, + address: address, + conn: conn, + requestChan: make(chan *request), + sessions: make(map[uint32]*Session), + } + tx := c.runTransmitter() + rx := c.runReceiver() + c.runDispatcher(tx, rx) + + return c, nil +} + +// Close tears down the client. +func (c *Client) Close() error { + if err := c.conn.Close(); err != nil { + return fmt.Errorf("close connection: %w", err) + } + return nil +} + +// Session sets up a new session. +func (c *Client) Session() (*Session, error) { + s := &Session{ + client: c, + timeout: c.Timeout, + } + if err := s.open(c.NameOID, c.Name); err != nil { + return nil, err + } + c.sessions[s.ID()] = s + + return s, nil +} + +func (c *Client) runTransmitter() chan *pdu.HeaderPacket { + tx := make(chan *pdu.HeaderPacket) + + go func() { + for headerPacket := range tx { + headerPacketBytes, err := headerPacket.MarshalBinary() + if err != nil { + log.Printf("marshal error: %v", err) + continue + } + writer := bufio.NewWriter(c.conn) + if _, err := writer.Write(headerPacketBytes); err != nil { + log.Printf("write error: %v", err) + continue + } + if err := writer.Flush(); err != nil { + log.Printf("flush error: %v", err) + continue + } + } + }() + + return tx +} + +func (c *Client) runReceiver() chan *pdu.HeaderPacket { + rx := make(chan *pdu.HeaderPacket) + + go func() { + mainLoop: + for { + reader := bufio.NewReader(c.conn) + headerBytes := make([]byte, pdu.HeaderSize) + if _, err := reader.Read(headerBytes); err != nil { + if opErr, ok := err.(*net.OpError); ok && strings.HasSuffix(opErr.Error(), "use of closed network connection") { + return + } + if err == io.EOF { + log.Printf("lost connection - try to re-connect ...") + reopenLoop: + for { + time.Sleep(c.ReconnectInterval) + conn, err := net.Dial(c.network, c.address) + if err != nil { + log.Printf("try to reconnect: %v", err) + continue reopenLoop + } + c.conn = conn + go func() { + for _, session := range c.sessions { + delete(c.sessions, session.ID()) + if err := session.reopen(); err != nil { + log.Printf("error during reopen session: %v", err) + return + } + c.sessions[session.ID()] = session + log.Printf("successful re-connected") + } + }() + continue mainLoop + } + } + panic(err) + } + + header := &pdu.Header{} + if err := header.UnmarshalBinary(headerBytes); err != nil { + panic(err) + } + + var packet pdu.Packet + switch header.Type { + case pdu.TypeResponse: + packet = &pdu.Response{} + case pdu.TypeGet: + packet = &pdu.Get{} + case pdu.TypeGetNext: + packet = &pdu.GetNext{} + default: + log.Printf("unhandled packet of type %s", header.Type) + } + + packetBytes := make([]byte, header.PayloadLength) + if _, err := reader.Read(packetBytes); err != nil { + panic(err) + } + + if err := packet.UnmarshalBinary(packetBytes); err != nil { + panic(err) + } + + rx <- &pdu.HeaderPacket{Header: header, Packet: packet} + } + }() + + return rx +} + +func (c *Client) runDispatcher(tx, rx chan *pdu.HeaderPacket) { + go func() { + currentPacketID := uint32(0) + responseChans := make(map[uint32]chan *pdu.HeaderPacket) + + for { + select { + case request := <-c.requestChan: + // log.Printf(">: %v", request) + request.headerPacket.Header.PacketID = currentPacketID + responseChans[currentPacketID] = request.responseChan + currentPacketID++ + + tx <- request.headerPacket + case headerPacket := <-rx: + // log.Printf("<: %v", headerPacket) + packetID := headerPacket.Header.PacketID + responseChan, ok := responseChans[packetID] + if ok { + responseChan <- headerPacket + delete(responseChans, packetID) + } else { + session, ok := c.sessions[headerPacket.Header.SessionID] + if ok { + tx <- session.handle(headerPacket) + } else { + log.Printf("got without session: %v", headerPacket) + } + } + } + } + }() +} + +func (c *Client) request(hp *pdu.HeaderPacket) *pdu.HeaderPacket { + responseChan := make(chan *pdu.HeaderPacket) + request := &request{ + headerPacket: hp, + responseChan: responseChan, + } + c.requestChan <- request + headerPacket := <-responseChan + return headerPacket +} diff --git a/go-agentx/environment_test.go b/go-agentx/environment_test.go new file mode 100644 index 0000000..a934af7 --- /dev/null +++ b/go-agentx/environment_test.go @@ -0,0 +1,52 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package agentx_test + +import ( + "io" + "log" + "os" + "os/exec" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/posteo/go-agentx" + "github.com/posteo/go-agentx/value" +) + +type environment struct { + client *agentx.Client + tearDown func() +} + +func setUpTestEnvironment(tb testing.TB) *environment { + cmd := exec.Command("snmpd", "-Lo", "-f", "-c", "snmpd.conf") + + stdout, err := cmd.StdoutPipe() + require.NoError(tb, err) + go func() { + io.Copy(os.Stdout, stdout) + }() + + log.Printf("run: %s", cmd) + require.NoError(tb, cmd.Start()) + time.Sleep(500 * time.Millisecond) + + client, err := agentx.Dial("tcp", "127.0.0.1:30705") + require.NoError(tb, err) + client.Timeout = 60 * time.Second + client.NameOID = value.MustParseOID("1.3.6.1.4.1.45995") + client.Name = "test client" + + return &environment{ + client: client, + tearDown: func() { + require.NoError(tb, client.Close()) + require.NoError(tb, cmd.Process.Kill()) + }, + } +} diff --git a/go-agentx/go.mod b/go-agentx/go.mod new file mode 100644 index 0000000..96d0590 --- /dev/null +++ b/go-agentx/go.mod @@ -0,0 +1,5 @@ +module github.com/posteo/go-agentx + +go 1.13 + +require github.com/stretchr/testify v1.6.1 diff --git a/go-agentx/go.sum b/go-agentx/go.sum new file mode 100644 index 0000000..1f1e7af --- /dev/null +++ b/go-agentx/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go-agentx/handler.go b/go-agentx/handler.go new file mode 100644 index 0000000..624f697 --- /dev/null +++ b/go-agentx/handler.go @@ -0,0 +1,17 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package agentx + +import ( + "github.com/posteo/go-agentx/pdu" + "github.com/posteo/go-agentx/value" +) + +// Handler defines an interface for a handler of events that +// might occure during a session. +type Handler interface { + Get(value.OID) (value.OID, pdu.VariableType, interface{}, error) + GetNext(value.OID, bool, value.OID) (value.OID, pdu.VariableType, interface{}, error) +} diff --git a/go-agentx/helper_test.go b/go-agentx/helper_test.go new file mode 100644 index 0000000..65b8582 --- /dev/null +++ b/go-agentx/helper_test.go @@ -0,0 +1,31 @@ +package agentx_test + +import ( + "fmt" + "os/exec" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func SNMPGet(tb testing.TB, oid string) string { + cmd := exec.Command("snmpget", "-v2c", "-cpublic", "-On", "127.0.0.1:30161", oid) + output, err := cmd.CombinedOutput() + require.NoError(tb, err) + return strings.TrimSpace(string(output)) +} + +func SNMPGetNext(tb testing.TB, oid string) string { + cmd := exec.Command("snmpgetnext", "-v2c", "-cpublic", "-On", "127.0.0.1:30161", oid) + output, err := cmd.CombinedOutput() + require.NoError(tb, err) + return strings.TrimSpace(string(output)) +} + +func SNMPGetBulk(tb testing.TB, oid string, nonRepeaters, maxRepetitions int) string { + cmd := exec.Command("snmpbulkget", "-v2c", "-cpublic", "-On", fmt.Sprintf("-Cn%d", nonRepeaters), fmt.Sprintf("-Cr%d", maxRepetitions), "127.0.0.1:30161", oid) + output, err := cmd.CombinedOutput() + require.NoError(tb, err) + return strings.TrimSpace(string(output)) +} diff --git a/go-agentx/list_handler.go b/go-agentx/list_handler.go new file mode 100644 index 0000000..7aa2c30 --- /dev/null +++ b/go-agentx/list_handler.go @@ -0,0 +1,66 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package agentx + +import ( + "github.com/posteo/go-agentx/pdu" + "github.com/posteo/go-agentx/value" +) + +// ListHandler is a helper that takes a list of oids and implements +// a default behaviour for that list. +type ListHandler struct { + oids []value.OID + items map[string]*ListItem +} + +// Add adds a list item for the provided oid and returns it. +func (l *ListHandler) Add(oid string) *ListItem { + if l.items == nil { + l.items = make(map[string]*ListItem) + } + + parsedOID := value.MustParseOID(oid) + l.oids = append(l.oids, parsedOID) + value.SortOIDs(l.oids) + item := &ListItem{} + l.items[oid] = item + return item +} + +// Get tries to find the provided oid and returns the corresponding value. +func (l *ListHandler) Get(oid value.OID) (value.OID, pdu.VariableType, interface{}, error) { + if l.items == nil { + return nil, pdu.VariableTypeNoSuchObject, nil, nil + } + + item, ok := l.items[oid.String()] + if ok { + return oid, item.Type, item.Value, nil + } + return nil, pdu.VariableTypeNoSuchObject, nil, nil +} + +// GetNext tries to find the value that follows the provided oid and returns it. +func (l *ListHandler) GetNext(from value.OID, includeFrom bool, to value.OID) (value.OID, pdu.VariableType, interface{}, error) { + if l.items == nil { + return nil, pdu.VariableTypeNoSuchObject, nil, nil + } + + for _, oid := range l.oids { + if oidWithin(oid, from, includeFrom, to) { + return l.Get(oid) + } + } + + return nil, pdu.VariableTypeNoSuchObject, nil, nil +} + +func oidWithin(oid value.OID, from value.OID, includeFrom bool, to value.OID) bool { + fromCompare := value.CompareOIDs(from, oid) + toCompare := value.CompareOIDs(to, oid) + + return (fromCompare == -1 || (fromCompare == 0 && includeFrom)) && (toCompare == 1) +} diff --git a/go-agentx/list_handler_test.go b/go-agentx/list_handler_test.go new file mode 100644 index 0000000..c79a548 --- /dev/null +++ b/go-agentx/list_handler_test.go @@ -0,0 +1,63 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package agentx_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/posteo/go-agentx" + "github.com/posteo/go-agentx/pdu" + "github.com/posteo/go-agentx/value" +) + +func TestListHandler(t *testing.T) { + e := setUpTestEnvironment(t) + defer e.tearDown() + + session, err := e.client.Session() + require.NoError(t, err) + defer session.Close() + + lh := &agentx.ListHandler{} + i := lh.Add("1.3.6.1.4.1.45995.3.1") + i.Type = pdu.VariableTypeOctetString + i.Value = "test" + session.Handler = lh + + baseOID := value.MustParseOID("1.3.6.1.4.1.45995") + + require.NoError(t, session.Register(127, baseOID)) + defer session.Unregister(127, baseOID) + + t.Run("Get", func(t *testing.T) { + assert.Equal(t, + ".1.3.6.1.4.1.45995.3.1 = STRING: \"test\"", + SNMPGet(t, "1.3.6.1.4.1.45995.3.1")) + + assert.Equal(t, + ".1.3.6.1.4.1.45995.3.2 = No Such Object available on this agent at this OID", + SNMPGet(t, "1.3.6.1.4.1.45995.3.2")) + }) + + t.Run("GetNext", func(t *testing.T) { + assert.Equal(t, + ".1.3.6.1.4.1.45995.3.1 = STRING: \"test\"", + SNMPGetNext(t, "1.3.6.1.4.1.45995.3.0")) + + assert.Equal(t, + ".1.3.6.1.4.1.45995.3.1 = STRING: \"test\"", + SNMPGetNext(t, "1.3.6.1.4.1.45995.3")) + + }) + + t.Run("GetBulk", func(t *testing.T) { + assert.Equal(t, + ".1.3.6.1.4.1.45995.3.1 = STRING: \"test\"", + SNMPGetBulk(t, "1.3.6.1.4.1.45995.3.0", 0, 1)) + }) +} diff --git a/go-agentx/list_item.go b/go-agentx/list_item.go new file mode 100644 index 0000000..84725d1 --- /dev/null +++ b/go-agentx/list_item.go @@ -0,0 +1,13 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package agentx + +import "github.com/posteo/go-agentx/pdu" + +// ListItem defines an item of the list handler. +type ListItem struct { + Type pdu.VariableType + Value interface{} +} diff --git a/go-agentx/marshaler/multi.go b/go-agentx/marshaler/multi.go new file mode 100644 index 0000000..290f2de --- /dev/null +++ b/go-agentx/marshaler/multi.go @@ -0,0 +1,33 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package marshaler + +import ( + "encoding" +) + +// Multi defines a binary marshaler that marshals all child marshalers +// and concatinate the results. +type Multi []encoding.BinaryMarshaler + +// NewMulti returns a new instance of MultiBinaryMarshaler. +func NewMulti(marshalers ...encoding.BinaryMarshaler) Multi { + return Multi(marshalers) +} + +// MarshalBinary marshals all the binary marshalers and concatinates the results. +func (m Multi) MarshalBinary() ([]byte, error) { + result := []byte{} + + for _, marshaler := range m { + data, err := marshaler.MarshalBinary() + if err != nil { + return nil, err + } + result = append(result, data...) + } + + return result, nil +} diff --git a/go-agentx/pdu/allocate_index.go b/go-agentx/pdu/allocate_index.go new file mode 100644 index 0000000..12f0589 --- /dev/null +++ b/go-agentx/pdu/allocate_index.go @@ -0,0 +1,29 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +// AllocateIndex defiens the pdu allocate index packet. +type AllocateIndex struct { + Variables Variables +} + +// Type returns the pdu packet type. +func (ai *AllocateIndex) Type() Type { + return TypeIndexAllocate +} + +// MarshalBinary returns the pdu packet as a slice of bytes. +func (ai *AllocateIndex) MarshalBinary() ([]byte, error) { + data, err := ai.Variables.MarshalBinary() + if err != nil { + return nil, err + } + return data, nil +} + +// UnmarshalBinary sets the packet structure from the provided slice of bytes. +func (ai *AllocateIndex) UnmarshalBinary(data []byte) error { + return nil +} diff --git a/go-agentx/pdu/close.go b/go-agentx/pdu/close.go new file mode 100644 index 0000000..6e1ed50 --- /dev/null +++ b/go-agentx/pdu/close.go @@ -0,0 +1,26 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +// Close defines the pdu close packet. +type Close struct { + Reason Reason +} + +// Type returns the pdu packet type. +func (c *Close) Type() Type { + return TypeClose +} + +// MarshalBinary returns the pdu packet as a slice of bytes. +func (c *Close) MarshalBinary() ([]byte, error) { + return []byte{byte(c.Reason), 0x00, 0x00, 0x00}, nil +} + +// UnmarshalBinary sets the packet structure from the provided slice of bytes. +func (c *Close) UnmarshalBinary(data []byte) error { + c.Reason = Reason(data[0]) + return nil +} diff --git a/go-agentx/pdu/deallocate_index.go b/go-agentx/pdu/deallocate_index.go new file mode 100644 index 0000000..9ac5bd3 --- /dev/null +++ b/go-agentx/pdu/deallocate_index.go @@ -0,0 +1,29 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +// DeallocateIndex defiens the pdu deallocate index packet. +type DeallocateIndex struct { + Variables Variables +} + +// Type returns the pdu packet type. +func (di *DeallocateIndex) Type() Type { + return TypeIndexDeallocate +} + +// MarshalBinary returns the pdu packet as a slice of bytes. +func (di *DeallocateIndex) MarshalBinary() ([]byte, error) { + data, err := di.Variables.MarshalBinary() + if err != nil { + return nil, err + } + return data, nil +} + +// UnmarshalBinary sets the packet structure from the provided slice of bytes. +func (di *DeallocateIndex) UnmarshalBinary(data []byte) error { + return nil +} diff --git a/go-agentx/pdu/error.go b/go-agentx/pdu/error.go new file mode 100644 index 0000000..41570ef --- /dev/null +++ b/go-agentx/pdu/error.go @@ -0,0 +1,62 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +import "fmt" + +// The various pdu packet errors. +const ( + ErrorNone Error = 0 + ErrorOpenFailed Error = 256 + ErrorNotOpen Error = 257 + ErrorIndexWrongType Error = 258 + ErrorIndexAlreadyAllocated Error = 259 + ErrorIndexNoneAvailable Error = 260 + ErrorIndexNotAllocated Error = 261 + ErrorUnsupportedContext Error = 262 + ErrorDuplicateRegistration Error = 263 + ErrorUnknownRegistration Error = 264 + ErrorUnknownAgentCaps Error = 265 + ErrorParse Error = 266 + ErrorRequestDenied Error = 267 + ErrorProcessing Error = 268 +) + +// Error defines a pdu packet error. +type Error uint16 + +func (e Error) String() string { + switch e { + case ErrorNone: + return "ErrorNone" + case ErrorOpenFailed: + return "ErrorOpenFailed" + case ErrorNotOpen: + return "ErrorNotOpen" + case ErrorIndexWrongType: + return "ErrorIndexWrongType" + case ErrorIndexAlreadyAllocated: + return "ErrorIndexAlreadyAllocated" + case ErrorIndexNoneAvailable: + return "ErrorIndexNoneAvailable" + case ErrorIndexNotAllocated: + return "ErrorIndexNotAllocated" + case ErrorUnsupportedContext: + return "ErrorUnsupportedContext" + case ErrorDuplicateRegistration: + return "ErrorDuplicateRegistration" + case ErrorUnknownRegistration: + return "ErrorUnknownRegistration" + case ErrorUnknownAgentCaps: + return "ErrorUnknownAgentCaps" + case ErrorParse: + return "ErrorParse" + case ErrorRequestDenied: + return "ErrorRequestDenied" + case ErrorProcessing: + return "ErrorProcessing" + } + return fmt.Sprintf("ErrorUnknown (%d)", e) +} diff --git a/go-agentx/pdu/flags.go b/go-agentx/pdu/flags.go new file mode 100644 index 0000000..1cebdac --- /dev/null +++ b/go-agentx/pdu/flags.go @@ -0,0 +1,45 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +import ( + "fmt" + "strings" +) + +// The various pdu packet flags. +const ( + FlagInstanceRegistration Flags = 1 << 0 + FlagNewIndex Flags = 1 << 1 + FlagAnyIndex Flags = 1 << 2 + FlagNonDefaultContext Flags = 1 << 3 + FlagNetworkByteOrder Flags = 1 << 4 +) + +// Flags defines pdu packet flags. +type Flags byte + +func (f Flags) String() string { + result := []string{} + if f&FlagInstanceRegistration != 0 { + result = append(result, "FlagInstanceRegistration") + } + if f&FlagNewIndex != 0 { + result = append(result, "FlagNewIndex") + } + if f&FlagAnyIndex != 0 { + result = append(result, "FlagAnyIndex") + } + if f&FlagNonDefaultContext != 0 { + result = append(result, "FlagNonDefaultContext") + } + if f&FlagNetworkByteOrder != 0 { + result = append(result, "FlagNetworkByteOrder") + } + if len(result) == 0 { + return "(FlagNone)" + } + return fmt.Sprintf("(%s)", strings.Join(result, " | ")) +} diff --git a/go-agentx/pdu/get.go b/go-agentx/pdu/get.go new file mode 100644 index 0000000..0d3d8e0 --- /dev/null +++ b/go-agentx/pdu/get.go @@ -0,0 +1,40 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +import "github.com/posteo/go-agentx/value" + +// Get defines the pdu get packet. +type Get struct { + SearchRange Range +} + +// GetOID returns the oid. +func (g *Get) GetOID() value.OID { + return g.SearchRange.From.GetIdentifier() +} + +// SetOID sets the provided oid. +func (g *Get) SetOID(oid value.OID) { + g.SearchRange.From.SetIdentifier(oid) +} + +// Type returns the pdu packet type. +func (g *Get) Type() Type { + return TypeGet +} + +// MarshalBinary returns the pdu packet as a slice of bytes. +func (g *Get) MarshalBinary() ([]byte, error) { + return []byte{}, nil +} + +// UnmarshalBinary sets the packet structure from the provided slice of bytes. +func (g *Get) UnmarshalBinary(data []byte) error { + if err := g.SearchRange.UnmarshalBinary(data); err != nil { + return err + } + return nil +} diff --git a/go-agentx/pdu/get_next.go b/go-agentx/pdu/get_next.go new file mode 100644 index 0000000..eea01f0 --- /dev/null +++ b/go-agentx/pdu/get_next.go @@ -0,0 +1,28 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +// GetNext defines the pdu get next packet. +type GetNext struct { + SearchRanges Ranges +} + +// Type returns the pdu packet type. +func (g *GetNext) Type() Type { + return TypeGetNext +} + +// MarshalBinary returns the pdu packet as a slice of bytes. +func (g *GetNext) MarshalBinary() ([]byte, error) { + return []byte{}, nil +} + +// UnmarshalBinary sets the packet structure from the provided slice of bytes. +func (g *GetNext) UnmarshalBinary(data []byte) error { + if err := g.SearchRanges.UnmarshalBinary(data); err != nil { + return err + } + return nil +} diff --git a/go-agentx/pdu/header.go b/go-agentx/pdu/header.go new file mode 100644 index 0000000..bbc361a --- /dev/null +++ b/go-agentx/pdu/header.go @@ -0,0 +1,61 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +import ( + "bytes" + "encoding/binary" + "fmt" +) + +const ( + // HeaderSize defines the total size of a header packet. + HeaderSize = 20 +) + +// Header defines a pdu packet header +type Header struct { + Version byte + Type Type + Flags Flags + SessionID uint32 + TransactionID uint32 + PacketID uint32 + PayloadLength uint32 +} + +// MarshalBinary returns the pdu header as a slice of bytes. +func (h *Header) MarshalBinary() ([]byte, error) { + buffer := bytes.NewBuffer([]byte{h.Version, byte(h.Type), byte(h.Flags), 0x00}) + + binary.Write(buffer, binary.LittleEndian, h.SessionID) + binary.Write(buffer, binary.LittleEndian, h.TransactionID) + binary.Write(buffer, binary.LittleEndian, h.PacketID) + binary.Write(buffer, binary.LittleEndian, h.PayloadLength) + + return buffer.Bytes(), nil +} + +// UnmarshalBinary sets the header structure from the provided slice of bytes. +func (h *Header) UnmarshalBinary(data []byte) error { + if len(data) < HeaderSize { + return fmt.Errorf("not enough bytes (%d) to unmarshal the header (%d)", len(data), HeaderSize) + } + + h.Version, h.Type, h.Flags = data[0], Type(data[1]), Flags(data[2]) + + buffer := bytes.NewBuffer(data[4:]) + + binary.Read(buffer, binary.LittleEndian, &h.SessionID) + binary.Read(buffer, binary.LittleEndian, &h.TransactionID) + binary.Read(buffer, binary.LittleEndian, &h.PacketID) + binary.Read(buffer, binary.LittleEndian, &h.PayloadLength) + + return nil +} + +func (h *Header) String() string { + return "(header " + h.Type.String() + ")" +} diff --git a/go-agentx/pdu/header_packet.go b/go-agentx/pdu/header_packet.go new file mode 100644 index 0000000..20b8aa3 --- /dev/null +++ b/go-agentx/pdu/header_packet.go @@ -0,0 +1,38 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +import ( + "fmt" +) + +// HeaderPacket defines a container structure for a header and a packet. +type HeaderPacket struct { + Header *Header + Packet Packet +} + +// MarshalBinary returns the pdu packet as a slice of bytes. +func (hp *HeaderPacket) MarshalBinary() ([]byte, error) { + payloadBytes, err := hp.Packet.MarshalBinary() + if err != nil { + return nil, err + } + + hp.Header.Version = 1 + hp.Header.Type = hp.Packet.Type() + hp.Header.PayloadLength = uint32(len(payloadBytes)) + + result, err := hp.Header.MarshalBinary() + if err != nil { + return nil, err + } + + return append(result, payloadBytes...), nil +} + +func (hp *HeaderPacket) String() string { + return fmt.Sprintf("[head %v, body %v]", hp.Header, hp.Packet) +} diff --git a/go-agentx/pdu/object_identifier.go b/go-agentx/pdu/object_identifier.go new file mode 100644 index 0000000..e8547ec --- /dev/null +++ b/go-agentx/pdu/object_identifier.go @@ -0,0 +1,96 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +import ( + "bytes" + "encoding/binary" + + "github.com/posteo/go-agentx/value" +) + +// ObjectIdentifier defines the pdu object identifier packet. +type ObjectIdentifier struct { + Prefix uint8 + Include byte + Subidentifiers []uint32 +} + +// SetInclude sets the include field. +func (o *ObjectIdentifier) SetInclude(value bool) { + if value { + o.Include = 0x01 + } else { + o.Include = 0x00 + } +} + +// GetInclude returns true if the include field ist set, false otherwise. +func (o *ObjectIdentifier) GetInclude() bool { + if o.Include == 0x00 { + return false + } + return true +} + +// SetIdentifier set the subidentifiers by the provided oid string. +func (o *ObjectIdentifier) SetIdentifier(oid value.OID) { + o.Subidentifiers = make([]uint32, 0) + + if len(oid) > 4 && oid[0] == 1 && oid[1] == 3 && oid[2] == 6 && oid[3] == 1 { + o.Subidentifiers = append(o.Subidentifiers, uint32(1), uint32(3), uint32(6), uint32(1), uint32(oid[4])) + oid = oid[5:] + } + + o.Subidentifiers = append(o.Subidentifiers, oid...) +} + +// GetIdentifier returns the identifier as an oid string. +func (o *ObjectIdentifier) GetIdentifier() value.OID { + var oid value.OID + if o.Prefix != 0 { + oid = append(oid, 1, 3, 6, 1, uint32(o.Prefix)) + } + return append(oid, o.Subidentifiers...) +} + +// ByteSize returns the number of bytes, the binding would need in the encoded version. +func (o *ObjectIdentifier) ByteSize() int { + return 4 + len(o.Subidentifiers)*4 +} + +// MarshalBinary returns the pdu packet as a slice of bytes. +func (o *ObjectIdentifier) MarshalBinary() ([]byte, error) { + buffer := bytes.NewBuffer([]byte{byte(len(o.Subidentifiers)), o.Prefix, o.Include, 0x00}) + + for _, subidentifier := range o.Subidentifiers { + binary.Write(buffer, binary.LittleEndian, &subidentifier) + } + + return buffer.Bytes(), nil +} + +// UnmarshalBinary sets the packet structure from the provided slice of bytes. +func (o *ObjectIdentifier) UnmarshalBinary(data []byte) error { + count := data[0] + o.Prefix = data[1] + o.Include = data[2] + + o.Subidentifiers = make([]uint32, 0) + buffer := bytes.NewBuffer(data[4:]) + for index := byte(0); index < count; index++ { + var subidentifier uint32 + if err := binary.Read(buffer, binary.LittleEndian, &subidentifier); err != nil { + return err + } + o.Subidentifiers = append(o.Subidentifiers, subidentifier) + } + + return nil +} + +func (o ObjectIdentifier) String() string { + return o.GetIdentifier().String() +} diff --git a/go-agentx/pdu/octet_string.go b/go-agentx/pdu/octet_string.go new file mode 100644 index 0000000..68bc5f4 --- /dev/null +++ b/go-agentx/pdu/octet_string.go @@ -0,0 +1,43 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +import ( + "bytes" + "encoding/binary" +) + +// OctetString defines the pdu description packet. +type OctetString struct { + Text string +} + +// MarshalBinary returns the pdu packet as a slice of bytes. +func (o *OctetString) MarshalBinary() ([]byte, error) { + buffer := &bytes.Buffer{} + + binary.Write(buffer, binary.LittleEndian, uint32(len(o.Text))) + buffer.WriteString(o.Text) + + for buffer.Len()%4 > 0 { + buffer.WriteByte(0x00) + } + + return buffer.Bytes(), nil +} + +// UnmarshalBinary sets the packet structure from the provided slice of bytes. +func (o *OctetString) UnmarshalBinary(data []byte) error { + buffer := bytes.NewBuffer(data) + + length := uint32(0) + if err := binary.Read(buffer, binary.LittleEndian, &length); err != nil { + return err + } + + o.Text = string(data[4 : 4+length]) + + return nil +} diff --git a/go-agentx/pdu/open.go b/go-agentx/pdu/open.go new file mode 100644 index 0000000..08a5d61 --- /dev/null +++ b/go-agentx/pdu/open.go @@ -0,0 +1,38 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +import ( + "github.com/posteo/go-agentx/marshaler" +) + +// Open defines a pdu open packet. +type Open struct { + Timeout Timeout + ID ObjectIdentifier + Description OctetString +} + +// Type returns the pdu packet type. +func (o *Open) Type() Type { + return TypeOpen +} + +// MarshalBinary returns the pdu packet as a slice of bytes. +func (o *Open) MarshalBinary() ([]byte, error) { + combined := marshaler.NewMulti(&o.Timeout, &o.ID, &o.Description) + + combinedBytes, err := combined.MarshalBinary() + if err != nil { + return nil, err + } + + return combinedBytes, nil +} + +// UnmarshalBinary sets the packet structure from the provided slice of bytes. +func (o *Open) UnmarshalBinary(data []byte) error { + return nil +} diff --git a/go-agentx/pdu/packet.go b/go-agentx/pdu/packet.go new file mode 100644 index 0000000..ee53711 --- /dev/null +++ b/go-agentx/pdu/packet.go @@ -0,0 +1,14 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +import "encoding" + +// Packet defines a general interface for a pdu packet. +type Packet interface { + TypeOwner + encoding.BinaryMarshaler + encoding.BinaryUnmarshaler +} diff --git a/go-agentx/pdu/range.go b/go-agentx/pdu/range.go new file mode 100644 index 0000000..acb651a --- /dev/null +++ b/go-agentx/pdu/range.go @@ -0,0 +1,53 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +import ( + "fmt" +) + +// Range defines the pdu search range packet. +type Range struct { + From ObjectIdentifier + To ObjectIdentifier +} + +// ByteSize returns the number of bytes, the binding would need in the encoded version. +func (r *Range) ByteSize() int { + return r.From.ByteSize() + r.To.ByteSize() +} + +// MarshalBinary returns the pdu packet as a slice of bytes. +func (r *Range) MarshalBinary() ([]byte, error) { + r.To.SetInclude(false) + return []byte{}, nil +} + +// UnmarshalBinary sets the packet structure from the provided slice of bytes. +func (r *Range) UnmarshalBinary(data []byte) error { + if err := r.From.UnmarshalBinary(data); err != nil { + return err + } + if err := r.To.UnmarshalBinary(data[r.From.ByteSize():]); err != nil { + return err + } + return nil +} + +func (r Range) String() string { + result := "" + if r.From.GetInclude() { + result += "[" + } else { + result += "(" + } + result += fmt.Sprintf("%v, %v", r.From, r.To) + if r.To.GetInclude() { + result += "]" + } else { + result += ")" + } + return result +} diff --git a/go-agentx/pdu/ranges.go b/go-agentx/pdu/ranges.go new file mode 100644 index 0000000..c404ed9 --- /dev/null +++ b/go-agentx/pdu/ranges.go @@ -0,0 +1,27 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +// Ranges defines the pdu search range list packet. +type Ranges []Range + +// MarshalBinary returns the pdu packet as a slice of bytes. +func (r *Ranges) MarshalBinary() ([]byte, error) { + return []byte{}, nil +} + +// UnmarshalBinary sets the packet structure from the provided slice of bytes. +func (r *Ranges) UnmarshalBinary(data []byte) error { + *r = make([]Range, 0) + for offset := 0; offset < len(data); { + rng := Range{} + if err := rng.UnmarshalBinary(data[offset:]); err != nil { + return err + } + *r = append(*r, rng) + offset += rng.ByteSize() + } + return nil +} diff --git a/go-agentx/pdu/reason.go b/go-agentx/pdu/reason.go new file mode 100644 index 0000000..687574b --- /dev/null +++ b/go-agentx/pdu/reason.go @@ -0,0 +1,38 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +import "fmt" + +// The various pdu packet reasons. +const ( + ReasonOther Reason = 1 + ReasonParseError Reason = 2 + ReasonProtocolError Reason = 3 + ReasonTimeouts Reason = 4 + ReasonShutdown Reason = 5 + ReasonByManager Reason = 6 +) + +// Reason defines a reason. +type Reason byte + +func (r Reason) String() string { + switch r { + case ReasonOther: + return "ReasonOther" + case ReasonParseError: + return "ReasonParseError" + case ReasonProtocolError: + return "ReasonProtocolError" + case ReasonTimeouts: + return "ReasonTimeouts" + case ReasonShutdown: + return "ReasonShutdown" + case ReasonByManager: + return "ReasonByManager" + } + return fmt.Sprintf("ReasonUnknown (%d)", r) +} diff --git a/go-agentx/pdu/register.go b/go-agentx/pdu/register.go new file mode 100644 index 0000000..5419c04 --- /dev/null +++ b/go-agentx/pdu/register.go @@ -0,0 +1,37 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +import ( + "github.com/posteo/go-agentx/marshaler" +) + +// Register defines the pdu register packet. +type Register struct { + Timeout Timeout + Subtree ObjectIdentifier +} + +// Type returns the pdu packet type. +func (r *Register) Type() Type { + return TypeRegister +} + +// MarshalBinary returns the pdu packet as a slice of bytes. +func (r *Register) MarshalBinary() ([]byte, error) { + combined := marshaler.NewMulti(&r.Timeout, &r.Subtree) + + combinedBytes, err := combined.MarshalBinary() + if err != nil { + return nil, err + } + + return combinedBytes, nil +} + +// UnmarshalBinary sets the packet structure from the provided slice of bytes. +func (r *Register) UnmarshalBinary(data []byte) error { + return nil +} diff --git a/go-agentx/pdu/response.go b/go-agentx/pdu/response.go new file mode 100644 index 0000000..972eb9a --- /dev/null +++ b/go-agentx/pdu/response.go @@ -0,0 +1,68 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +import ( + "bytes" + "encoding/binary" + "time" +) + +// Response defines the pdu response packet. +type Response struct { + UpTime time.Duration + Error Error + Index uint16 + Variables Variables +} + +// Type returns the pdu packet type. +func (r *Response) Type() Type { + return TypeResponse +} + +// MarshalBinary returns the pdu packet as a slice of bytes. +func (r *Response) MarshalBinary() ([]byte, error) { + buffer := &bytes.Buffer{} + + upTime := uint32(r.UpTime.Seconds() / 100) + binary.Write(buffer, binary.LittleEndian, &upTime) + binary.Write(buffer, binary.LittleEndian, &r.Error) + binary.Write(buffer, binary.LittleEndian, &r.Index) + + vBytes, err := r.Variables.MarshalBinary() + if err != nil { + return nil, err + } + buffer.Write(vBytes) + + return buffer.Bytes(), nil +} + +// UnmarshalBinary sets the packet structure from the provided slice of bytes. +func (r *Response) UnmarshalBinary(data []byte) error { + buffer := bytes.NewBuffer(data) + + upTime := uint32(0) + if err := binary.Read(buffer, binary.LittleEndian, &upTime); err != nil { + return err + } + r.UpTime = time.Second * time.Duration(upTime*100) + if err := binary.Read(buffer, binary.LittleEndian, &r.Error); err != nil { + return err + } + if err := binary.Read(buffer, binary.LittleEndian, &r.Index); err != nil { + return err + } + if err := r.Variables.UnmarshalBinary(data[8:]); err != nil { + return err + } + + return nil +} + +func (r *Response) String() string { + return "(response " + r.Variables.String() + ")" +} diff --git a/go-agentx/pdu/timeout.go b/go-agentx/pdu/timeout.go new file mode 100644 index 0000000..4dd6536 --- /dev/null +++ b/go-agentx/pdu/timeout.go @@ -0,0 +1,29 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +import "time" + +// Timeout defines the pdu timeout packet. +type Timeout struct { + Duration time.Duration + Priority byte +} + +// MarshalBinary returns the pdu packet as a slice of bytes. +func (t *Timeout) MarshalBinary() ([]byte, error) { + return []byte{byte(t.Duration.Seconds()), t.Priority, 0x00, 0x00}, nil +} + +// UnmarshalBinary sets the packet structure from the provided slice of bytes. +func (t *Timeout) UnmarshalBinary(data []byte) error { + t.Duration = time.Duration(data[0]) * time.Second + t.Priority = data[1] + return nil +} + +func (t Timeout) String() string { + return t.Duration.String() +} diff --git a/go-agentx/pdu/type.go b/go-agentx/pdu/type.go new file mode 100644 index 0000000..f1570ad --- /dev/null +++ b/go-agentx/pdu/type.go @@ -0,0 +1,77 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +// The various pdu packet types. +const ( + TypeOpen Type = 1 + TypeClose Type = 2 + TypeRegister Type = 3 + TypeUnregister Type = 4 + TypeGet Type = 5 + TypeGetNext Type = 6 + TypeGetBulk Type = 7 + TypeTestSet Type = 8 + TypeCommitSet Type = 9 + TypeUndoSet Type = 10 + TypeCleanupSet Type = 11 + TypeNotify Type = 12 + TypePing Type = 13 + TypeIndexAllocate Type = 14 + TypeIndexDeallocate Type = 15 + TypeAddAgentCaps Type = 16 + TypeRemoveAgentCaps Type = 17 + TypeResponse Type = 18 +) + +// Type defines the pdu packet type. +type Type byte + +// TypeOwner defines the interface for an object that provides a type. +type TypeOwner interface { + Type() Type +} + +func (t Type) String() string { + switch t { + case TypeOpen: + return "TypeOpen" + case TypeClose: + return "TypeClose" + case TypeRegister: + return "TypeRegister" + case TypeUnregister: + return "TypeUnregister" + case TypeGet: + return "TypeGet" + case TypeGetNext: + return "TypeGetNext" + case TypeGetBulk: + return "TypeGetBulk" + case TypeTestSet: + return "TypeTestSet" + case TypeCommitSet: + return "TypeCommitSet" + case TypeUndoSet: + return "TypeUndoSet" + case TypeCleanupSet: + return "TypeCleanupSet" + case TypeNotify: + return "TypeNotify" + case TypePing: + return "TypePing" + case TypeIndexAllocate: + return "TypeIndexAllocate" + case TypeIndexDeallocate: + return "TypeIndexDeallocate" + case TypeAddAgentCaps: + return "TypeAddAgentCaps" + case TypeRemoveAgentCaps: + return "TypeRemoveAgentCaps" + case TypeResponse: + return "TypeResponse" + } + return "TypeUnknown" +} diff --git a/go-agentx/pdu/unregister.go b/go-agentx/pdu/unregister.go new file mode 100644 index 0000000..3603450 --- /dev/null +++ b/go-agentx/pdu/unregister.go @@ -0,0 +1,37 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +import ( + "github.com/posteo/go-agentx/marshaler" +) + +// Unregister defines the pdu unregister packet. +type Unregister struct { + Timeout Timeout + Subtree ObjectIdentifier +} + +// Type returns the pdu packet type. +func (u *Unregister) Type() Type { + return TypeUnregister +} + +// MarshalBinary returns the pdu packet as a slice of bytes. +func (u *Unregister) MarshalBinary() ([]byte, error) { + combined := marshaler.NewMulti(&u.Timeout, &u.Subtree) + + combinedBytes, err := combined.MarshalBinary() + if err != nil { + return nil, err + } + + return combinedBytes, nil +} + +// UnmarshalBinary sets the packet structure from the provided slice of bytes. +func (u *Unregister) UnmarshalBinary(data []byte) error { + return nil +} diff --git a/go-agentx/pdu/variable.go b/go-agentx/pdu/variable.go new file mode 100644 index 0000000..6b45ace --- /dev/null +++ b/go-agentx/pdu/variable.go @@ -0,0 +1,185 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +import ( + "bytes" + "encoding/binary" + "fmt" + "net" + "time" + + "github.com/posteo/go-agentx/value" +) + +// Variable defines the pdu varbind packet. +type Variable struct { + Type VariableType + Name ObjectIdentifier + Value interface{} +} + +// Set sets the variable. +func (v *Variable) Set(oid value.OID, t VariableType, value interface{}) { + v.Name.SetIdentifier(oid) + v.Type = t + v.Value = value +} + +// ByteSize returns the number of bytes, the binding would need in the encoded version. +func (v *Variable) ByteSize() int { + bytes, err := v.MarshalBinary() + if err != nil { + panic(err) + } + return len(bytes) +} + +// MarshalBinary returns the pdu packet as a slice of bytes. +func (v *Variable) MarshalBinary() ([]byte, error) { + buffer := &bytes.Buffer{} + + binary.Write(buffer, binary.LittleEndian, &v.Type) + buffer.WriteByte(0x00) + buffer.WriteByte(0x00) + + nameBytes, err := v.Name.MarshalBinary() + if err != nil { + return nil, err + } + buffer.Write(nameBytes) + + switch v.Type { + case VariableTypeInteger: + value := v.Value.(int32) + binary.Write(buffer, binary.LittleEndian, &value) + case VariableTypeOctetString: + octetString := &OctetString{Text: v.Value.(string)} + octetStringBytes, err := octetString.MarshalBinary() + if err != nil { + return nil, err + } + buffer.Write(octetStringBytes) + case VariableTypeNull, VariableTypeNoSuchObject, VariableTypeNoSuchInstance, VariableTypeEndOfMIBView: + break + case VariableTypeObjectIdentifier: + targetOID, err := value.ParseOID(v.Value.(string)) + if err != nil { + return nil, err + } + + oi := &ObjectIdentifier{} + oi.SetIdentifier(targetOID) + oiBytes, err := oi.MarshalBinary() + if err != nil { + return nil, err + } + buffer.Write(oiBytes) + case VariableTypeIPAddress: + ip := v.Value.(net.IP) + octetString := &OctetString{Text: string(ip)} + octetStringBytes, err := octetString.MarshalBinary() + if err != nil { + return nil, err + } + buffer.Write(octetStringBytes) + case VariableTypeCounter32, VariableTypeGauge32: + value := v.Value.(uint32) + binary.Write(buffer, binary.LittleEndian, &value) + case VariableTypeTimeTicks: + value := uint32(v.Value.(time.Duration).Seconds() * 100) + binary.Write(buffer, binary.LittleEndian, &value) + case VariableTypeOpaque: + octetString := &OctetString{Text: string(v.Value.([]byte))} + octetStringBytes, err := octetString.MarshalBinary() + if err != nil { + return nil, err + } + buffer.Write(octetStringBytes) + case VariableTypeCounter64: + value := v.Value.(uint64) + binary.Write(buffer, binary.LittleEndian, &value) + default: + return nil, fmt.Errorf("unhandled variable type %s", v.Type) + } + + return buffer.Bytes(), nil +} + +// UnmarshalBinary sets the packet structure from the provided slice of bytes. +func (v *Variable) UnmarshalBinary(data []byte) error { + buffer := bytes.NewBuffer(data) + + if err := binary.Read(buffer, binary.LittleEndian, &v.Type); err != nil { + return err + } + offset := 4 + + if err := v.Name.UnmarshalBinary(data[offset:]); err != nil { + return err + } + offset += v.Name.ByteSize() + + switch v.Type { + case VariableTypeInteger: + value := int32(0) + if err := binary.Read(buffer, binary.LittleEndian, &value); err != nil { + return err + } + v.Value = value + case VariableTypeOctetString: + octetString := &OctetString{} + if err := octetString.UnmarshalBinary(data[offset:]); err != nil { + return err + } + v.Value = octetString.Text + case VariableTypeNull, VariableTypeNoSuchObject, VariableTypeNoSuchInstance, VariableTypeEndOfMIBView: + v.Value = nil + case VariableTypeObjectIdentifier: + oid := &ObjectIdentifier{} + if err := oid.UnmarshalBinary(data[offset:]); err != nil { + return err + } + v.Value = oid.GetIdentifier() + case VariableTypeIPAddress: + octetString := &OctetString{} + if err := octetString.UnmarshalBinary(data[offset:]); err != nil { + return err + } + v.Value = net.IP(octetString.Text) + case VariableTypeCounter32, VariableTypeGauge32: + value := uint32(0) + if err := binary.Read(buffer, binary.LittleEndian, &value); err != nil { + return err + } + v.Value = value + case VariableTypeTimeTicks: + value := uint32(0) + if err := binary.Read(buffer, binary.LittleEndian, &value); err != nil { + return err + } + v.Value = time.Duration(value) * time.Second / 100 + case VariableTypeOpaque: + octetString := &OctetString{} + if err := octetString.UnmarshalBinary(data[offset:]); err != nil { + return err + } + v.Value = []byte(octetString.Text) + case VariableTypeCounter64: + value := uint64(0) + if err := binary.Read(buffer, binary.LittleEndian, &value); err != nil { + return err + } + v.Value = value + default: + return fmt.Errorf("unhandled variable type %s", v.Type) + } + + return nil +} + +func (v *Variable) String() string { + return fmt.Sprintf("(variable %s = %v)", v.Type, v.Value) +} diff --git a/go-agentx/pdu/variable_type.go b/go-agentx/pdu/variable_type.go new file mode 100644 index 0000000..5513541 --- /dev/null +++ b/go-agentx/pdu/variable_type.go @@ -0,0 +1,59 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +import "fmt" + +// The various variable types. +const ( + VariableTypeInteger VariableType = 2 + VariableTypeOctetString VariableType = 4 + VariableTypeNull VariableType = 5 + VariableTypeObjectIdentifier VariableType = 6 + VariableTypeIPAddress VariableType = 64 + VariableTypeCounter32 VariableType = 65 + VariableTypeGauge32 VariableType = 66 + VariableTypeTimeTicks VariableType = 67 + VariableTypeOpaque VariableType = 68 + VariableTypeCounter64 VariableType = 70 + VariableTypeNoSuchObject VariableType = 128 + VariableTypeNoSuchInstance VariableType = 129 + VariableTypeEndOfMIBView VariableType = 130 +) + +// VariableType defines the type of a variable. +type VariableType uint16 + +func (v VariableType) String() string { + switch v { + case VariableTypeInteger: + return "VariableTypeInteger" + case VariableTypeOctetString: + return "VariableTypeOctetString" + case VariableTypeNull: + return "VariableTypeNull" + case VariableTypeObjectIdentifier: + return "VariableTypeObjectIdentifier" + case VariableTypeIPAddress: + return "VariableTypeIPAddress" + case VariableTypeCounter32: + return "VariableTypeCounter32" + case VariableTypeGauge32: + return "VariableTypeGauge32" + case VariableTypeTimeTicks: + return "VariableTypeTimeTicks" + case VariableTypeOpaque: + return "VariableTypeOpaque" + case VariableTypeCounter64: + return "VariableTypeCounter64" + case VariableTypeNoSuchObject: + return "VariableTypeNoSuchObject" + case VariableTypeNoSuchInstance: + return "VariableTypeNoSuchInstance" + case VariableTypeEndOfMIBView: + return "VariableTypeEndOfMIBView" + } + return fmt.Sprintf("VariableTypeUnknown (%d)", v) +} diff --git a/go-agentx/pdu/variables.go b/go-agentx/pdu/variables.go new file mode 100644 index 0000000..399c683 --- /dev/null +++ b/go-agentx/pdu/variables.go @@ -0,0 +1,56 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package pdu + +import ( + "strings" + + "github.com/posteo/go-agentx/value" +) + +// Variables defines a list of variable bindings. +type Variables []Variable + +// Add adds the provided variable. +func (v *Variables) Add(oid value.OID, t VariableType, value interface{}) { + variable := Variable{} + variable.Set(oid, t, value) + *v = append(*v, variable) +} + +// MarshalBinary returns the pdu packet as a slice of bytes. +func (v *Variables) MarshalBinary() ([]byte, error) { + result := []byte{} + for _, variable := range *v { + data, err := variable.MarshalBinary() + if err != nil { + return nil, err + } + result = append(result, data...) + } + return result, nil +} + +// UnmarshalBinary sets the packet structure from the provided slice of bytes. +func (v *Variables) UnmarshalBinary(data []byte) error { + *v = make([]Variable, 0) + for offset := 0; offset < len(data); { + variable := Variable{} + if err := variable.UnmarshalBinary(data[offset:]); err != nil { + return err + } + *v = append(*v, variable) + offset += variable.ByteSize() + } + return nil +} + +func (v Variables) String() string { + parts := make([]string, len(v)) + for index, va := range v { + parts[index] = va.String() + } + return "[variables " + strings.Join(parts, ", ") + "]" +} diff --git a/go-agentx/request.go b/go-agentx/request.go new file mode 100644 index 0000000..2526344 --- /dev/null +++ b/go-agentx/request.go @@ -0,0 +1,16 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package agentx + +import "github.com/posteo/go-agentx/pdu" + +type request struct { + headerPacket *pdu.HeaderPacket + responseChan chan *pdu.HeaderPacket +} + +func (r *request) String() string { + return "(request " + r.headerPacket.String() + ")" +} diff --git a/go-agentx/session.go b/go-agentx/session.go new file mode 100644 index 0000000..c43de9c --- /dev/null +++ b/go-agentx/session.go @@ -0,0 +1,185 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package agentx + +import ( + "errors" + "fmt" + "log" + "time" + + "github.com/posteo/go-agentx/pdu" + "github.com/posteo/go-agentx/value" +) + +// Session defines an agentx session. +type Session struct { + Handler Handler + + client *Client + sessionID uint32 + timeout time.Duration + + openRequestPacket *pdu.HeaderPacket + registerRequestPacket *pdu.HeaderPacket +} + +// ID returns the session id. +func (s *Session) ID() uint32 { + return s.sessionID +} + +// Register registers the client under the provided rootID with the provided priority +// on the master agent. +func (s *Session) Register(priority byte, baseOID value.OID) error { + if s.registerRequestPacket != nil { + return fmt.Errorf("session is already registered") + } + + requestPacket := &pdu.Register{} + requestPacket.Timeout.Duration = s.timeout + requestPacket.Timeout.Priority = priority + requestPacket.Subtree.SetIdentifier(baseOID) + request := &pdu.HeaderPacket{Header: &pdu.Header{Type: pdu.TypeRegister}, Packet: requestPacket} + + response := s.request(request) + if err := checkError(response); err != nil { + return err + } + s.registerRequestPacket = request + return nil +} + +// Unregister removes the registration for the provided subtree. +func (s *Session) Unregister(priority byte, baseOID value.OID) error { + if s.registerRequestPacket == nil { + return fmt.Errorf("session is not registered") + } + + requestPacket := &pdu.Unregister{} + requestPacket.Timeout.Duration = s.timeout + requestPacket.Timeout.Priority = priority + requestPacket.Subtree.SetIdentifier(baseOID) + request := &pdu.HeaderPacket{Header: &pdu.Header{}, Packet: requestPacket} + + response := s.request(request) + if err := checkError(response); err != nil { + return err + } + s.registerRequestPacket = nil + return nil +} + +// Close tears down the session with the master agent. +func (s *Session) Close() error { + requestPacket := &pdu.Close{Reason: pdu.ReasonShutdown} + + response := s.request(&pdu.HeaderPacket{Header: &pdu.Header{}, Packet: requestPacket}) + if err := checkError(response); err != nil { + return err + } + return nil +} + +func (s *Session) open(nameOID value.OID, name string) error { + requestPacket := &pdu.Open{} + requestPacket.Timeout.Duration = s.timeout + requestPacket.ID.SetIdentifier(nameOID) + requestPacket.Description.Text = name + request := &pdu.HeaderPacket{Header: &pdu.Header{Type: pdu.TypeOpen}, Packet: requestPacket} + + response := s.request(request) + if err := checkError(response); err != nil { + return err + } + s.sessionID = response.Header.SessionID + s.openRequestPacket = request + return nil +} + +func (s *Session) reopen() error { + if s.openRequestPacket != nil { + response := s.request(s.openRequestPacket) + if err := checkError(response); err != nil { + return err + } + s.sessionID = response.Header.SessionID + } + + if s.registerRequestPacket != nil { + response := s.request(s.registerRequestPacket) + if err := checkError(response); err != nil { + return err + } + } + + return nil +} + +func (s *Session) request(hp *pdu.HeaderPacket) *pdu.HeaderPacket { + hp.Header.SessionID = s.sessionID + return s.client.request(hp) +} + +func (s *Session) handle(request *pdu.HeaderPacket) *pdu.HeaderPacket { + responseHeader := &pdu.Header{} + responseHeader.SessionID = request.Header.SessionID + responseHeader.TransactionID = request.Header.TransactionID + responseHeader.PacketID = request.Header.PacketID + responsePacket := &pdu.Response{} + + switch requestPacket := request.Packet.(type) { + case *pdu.Get: + if s.Handler == nil { + log.Printf("warning: no handler for session specified") + responsePacket.Variables.Add(requestPacket.GetOID(), pdu.VariableTypeNull, nil) + } else { + oid, t, v, err := s.Handler.Get(requestPacket.GetOID()) + if err != nil { + log.Printf("error while handling packet: %v", err) + responsePacket.Error = pdu.ErrorProcessing + } + if oid == nil { + responsePacket.Variables.Add(requestPacket.GetOID(), pdu.VariableTypeNoSuchObject, nil) + } else { + responsePacket.Variables.Add(oid, t, v) + } + } + case *pdu.GetNext: + if s.Handler == nil { + log.Printf("warning: no handler for session specified") + } else { + for _, sr := range requestPacket.SearchRanges { + oid, t, v, err := s.Handler.GetNext(sr.From.GetIdentifier(), (sr.From.Include == 1), sr.To.GetIdentifier()) + if err != nil { + log.Printf("error while handling packet: %v", err) + responsePacket.Error = pdu.ErrorProcessing + } + + if oid == nil { + responsePacket.Variables.Add(sr.From.GetIdentifier(), pdu.VariableTypeEndOfMIBView, nil) + } else { + responsePacket.Variables.Add(oid, t, v) + } + } + } + default: + log.Printf("cannot handle unrequested packet: %v", request) + responsePacket.Error = pdu.ErrorProcessing + } + + return &pdu.HeaderPacket{Header: responseHeader, Packet: responsePacket} +} + +func checkError(hp *pdu.HeaderPacket) error { + response, ok := hp.Packet.(*pdu.Response) + if !ok { + return nil + } + if response.Error == pdu.ErrorNone { + return nil + } + return errors.New(response.Error.String()) +} diff --git a/go-agentx/session_test.go b/go-agentx/session_test.go new file mode 100644 index 0000000..4f90af5 --- /dev/null +++ b/go-agentx/session_test.go @@ -0,0 +1,48 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package agentx_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/posteo/go-agentx/value" +) + +func TestSession(t *testing.T) { + e := setUpTestEnvironment(t) + defer e.tearDown() + + t.Run("Open", func(t *testing.T) { + session, err := e.client.Session() + require.NoError(t, err) + defer session.Close() + + assert.NotEqual(t, 0, session.ID()) + }) + + t.Run("Close", func(t *testing.T) { + session, err := e.client.Session() + require.NoError(t, err) + + require.NoError(t, session.Close()) + }) + + t.Run("Register", func(t *testing.T) { + session, err := e.client.Session() + require.NoError(t, err) + defer session.Close() + + baseOID := value.MustParseOID("1.3.6.1.4.1.45995") + + require.NoError(t, + session.Register(127, baseOID)) + + require.NoError(t, + session.Unregister(127, baseOID)) + }) +} diff --git a/go-agentx/shell.nix b/go-agentx/shell.nix new file mode 100644 index 0000000..deff337 --- /dev/null +++ b/go-agentx/shell.nix @@ -0,0 +1,7 @@ +{ pkgs ? import {} }: +pkgs.mkShell { + name = "dev-environment"; + buildInputs = [ + pkgs.net-snmp + ]; +} diff --git a/go-agentx/snmpd.conf b/go-agentx/snmpd.conf new file mode 100644 index 0000000..d8133f0 --- /dev/null +++ b/go-agentx/snmpd.conf @@ -0,0 +1,7 @@ + +agentaddress udp:127.0.0.1:30161 + +rocommunity public + +master agentx +agentXSocket tcp:127.0.0.1:30705 \ No newline at end of file diff --git a/go-agentx/value/oid.go b/go-agentx/value/oid.go new file mode 100644 index 0000000..ae38ef4 --- /dev/null +++ b/go-agentx/value/oid.go @@ -0,0 +1,103 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package value + +import ( + "fmt" + "sort" + "strconv" + "strings" +) + +// OID defines an OID. +type OID []uint32 + +// ParseOID parses the provided string and returns a valid oid. If one of the +// subidentifers canot be parsed to an uint32, the function will panic. +func ParseOID(text string) (OID, error) { + var result OID + + parts := strings.Split(text, ".") + for _, part := range parts { + subidentifier, err := strconv.ParseUint(part, 10, 32) + if err != nil { + return nil, fmt.Errorf("parse uint [%s]: %w", part, err) + } + result = append(result, uint32(subidentifier)) + } + + return result, nil +} + +// MustParseOID works like ParseOID expect it panics on a parsing error. +func MustParseOID(text string) OID { + result, err := ParseOID(text) + if err != nil { + panic(err) + } + return result +} + +// First returns the first n subidentifiers as a new oid. +func (o OID) First(count int) OID { + return o[:count] +} + +// CommonPrefix compares the oid with the provided one and +// returns a new oid containing all matching prefix subidentifiers. +func (o OID) CommonPrefix(other OID) OID { + matchCount := 0 + + for index, subidentifier := range o { + if index >= len(other) || subidentifier != other[index] { + break + } + matchCount++ + } + + return o[:matchCount] +} + +// CompareOIDs returns an integer comparing two OIDs lexicographically. +// The result will be 0 if oid1 == oid2, -1 if oid1 < oid2, +1 if oid1 > oid2. +func CompareOIDs(oid1, oid2 OID) int { + if oid2 != nil { + oid1Length := len(oid1) + oid2Length := len(oid2) + for i := 0; i < oid1Length && i < oid2Length; i++ { + if oid1[i] < oid2[i] { + return -1 + } + if oid1[i] > oid2[i] { + return 1 + } + } + if oid1Length == oid2Length { + return 0 + } else if oid1Length < oid2Length { + return -1 + } else { + return 1 + } + } + return 1 +} + +// SortOIDs performs sorting of the OID list. +func SortOIDs(oids []OID) { + sort.Slice(oids, func(i, j int) bool { + return CompareOIDs(oids[i], oids[j]) == -1 + }) +} + +func (o OID) String() string { + var parts []string + + for _, subidentifier := range o { + parts = append(parts, fmt.Sprintf("%d", subidentifier)) + } + + return strings.Join(parts, ".") +} diff --git a/go-agentx/value/oid_test.go b/go-agentx/value/oid_test.go new file mode 100644 index 0000000..aac02f9 --- /dev/null +++ b/go-agentx/value/oid_test.go @@ -0,0 +1,70 @@ +// Copyright 2018 The agentx authors +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package value_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/posteo/go-agentx/value" +) + +func TestCommonPrefix(t *testing.T) { + oid := value.MustParseOID("1.3.6.1.2") + result := oid.CommonPrefix(value.MustParseOID("1.3.6.1.4")) + assert.Equal(t, value.MustParseOID("1.3.6.1"), result) +} + +func TestCompareOIDs_Less(t *testing.T) { + oid1 := value.OID{1, 3, 6, 1, 2} + oid2 := value.OID{1, 3, 6, 1, 4} + + // oid1 < oid2 + expected := -1 + assert.Equal(t, expected, value.CompareOIDs(oid1, oid2)) +} + +func TestCompareOIDs_Greater(t *testing.T) { + oid1 := value.OID{1, 3, 6, 1, 2} + oid2 := value.OID{1, 3, 6, 1, 4} + + // oid2 > oid1 + expected := 1 + assert.Equal(t, expected, value.CompareOIDs(oid2, oid1)) +} + +func TestCompareOIDs_Equals(t *testing.T) { + oid1 := value.OID{1, 3, 6, 1, 4} + oid2 := value.OID{1, 3, 6, 1, 4} + + // oid1 == oid2 + expected := 0 + assert.Equal(t, expected, value.CompareOIDs(oid1, oid2)) +} + +func TestCompareOIDs_NilValue(t *testing.T) { + oid1 := value.OID{1, 3, 6, 1, 4} + var oid2 value.OID + + // oid2 is nil, thus oid1 is greater + expected := 1 + assert.Equal(t, expected, value.CompareOIDs(oid1, oid2)) +} + +func TestSortOIDs(t *testing.T) { + var oidList []value.OID + oid1 := value.OID{1, 3, 6, 1} + oid2 := value.OID{1, 3, 6, 5, 7} + oid3 := value.OID{1, 3, 6, 1, 12} + oid4 := value.OID{1, 3, 6, 5} + + oidList = append(oidList, oid1, oid2, oid3, oid4) + value.SortOIDs(oidList) + + var expect []value.OID + expect = append(expect, oid1, oid3, oid4, oid2) + assert.Equal(t, expect, oidList) +}