Transport Implementation Notes

From Net-SNMP Wiki
Jump to: navigation, search

Introduction

Net-SNMP uses a layer of abstraction over the various transport protocol that are supported, which allows the SNMP specific portions of the library to be kept clean of the details of sending and receiving data for the transport protocols. Adding support for a new transport is relatively simple, and does not require any knowledge of the internals of the SNMP portions of the library.

Transports are uniquely identified by a domain object identifier. The standard domains are defined in the transport mappings MIB, SNMPv2-TM. Initial transports included UDP, DDP and IPX. Other transports, such as the DTLSUDP transport, were defined in later modules. Enterprises may define their own transport domains in their own enterprise MIBs.

When registering a transport domains with Net-SNMP, prefixes can be specified as well, allowing users to easily specify transport domains on the command line, without having to use a length object identifier. For example, to use the secure DTLS over UDP transport defined in RFC 5953, the user could use the following command:

snmpget dtlsudp:hostname sysContact.0

The implementation for the DTLSUDP transport would automatically take care of the details, including using the correct port (in this case, 10161).

How the configure script finds transports

For the Net-SNMP configure script to properly find and call the initialization function, the function must be named appropriately. If you are defining the XYZ transport, the source file and header must be named snmpXYZDomain.c and snmpXYZDomain.h, respectively. The initialization function must be named netsnmp_xyz_ctor. Note the the transport name is converted to lower case. This will allow configure to find and include the XYZ transport when --with-transports=XYZ is specified.

If the XYZ transport depends on another transport, the snmpXYZDomain header must use the config_require macro. For example, if the XYZ transport requires the UDP transport, the line

config_require(UDP)

will result in configure also including snmpUDPDomain.c.

All transport code files must be placed in the snmplib/transports directory.

Creating a transport

The Net-SNMP transport structure has several function pointers that a new transport must implement, and others that may be implemented as needed. All of these functions take the transport's pointer as the first argument.

Registering with snmplib

The transport initialization function (e.g. netsnmp_xyz_ctor) will be called at startup. This functions is responsible for registering the transport with the library. This is also where one would register any transport specific configuration tokens.

void netsnmp_xyz_ctor(void) {
    static netsnmp_tdomain XYZDomain;

    /* OID/length for transport OID */
    XYZDomain.name = XYZDomain_oid;
    XYZDomain.name_length = XYZDomain_oid_length;

    /* NULL terminated array of prefix(es) */
    XYZDomain.prefix = (const char **)calloc(2, sizeof(char *));;
    XYZDomain.prefix[0] = "xyz";

    XYZDomain.f_create_from_tstring_new = netsnmp_xyz_create_tstring;
    XYZDomain.f_create_from_ostring = netsnmp_xyz_create_ostring;

    /* register domain with snmplib */
    netsnmp_tdomain_register(&xyzDomain);

    register_config_handler("snmp", "xyzPDQretry", _parse_xyzPDQ,
                            NULL, NULL);
}

creating a transport

When registering a transport domain, the f_create_from_tstring_new function pointer must be set to a function that can create a netsnmp_transport from a user-supplied text string. The function is also passed a flag indicating if the transport is being opened for remote use (i.e. an outgoing connection) or local use (i.e. incoming connections, or server mode). Lastly, a default target may be provided, allowing ...

[TBD: discuss default_target]

The f_create_from_ostring function pointer must be set to a function that can create a transport from a user specified opaque data pointer with its length. This data pointer should contain an address in a format that the domain can recognize and parse.

netsnmp_transport structure

These functions should allocate and return a netsnmp_transport structure. A brief description is given below for the various structure fields.

  • data / data_length: generally, a pointer (and its length) to the address extracted from the specified transport string.
  • domain / domain_length: a pointer (and its length) to the domain OID
  • sock: the socket file descriptor for the transport. Note that it is possible to delay the creating/opening of the socket until the transport is actually used.
  • local: the local address (only for local/server mode)
  • remote: the remote address (only for remote/client mode)
  • msgMaxSize: the size of the largest message that the transport can send.
  • flags. Examples:
 NETSNMP_TRANSPORT_FLAG_STREAM : a stream based (as opposed to packet
                                 based) transport.
 NETSNMP_TRANSPORT_FLAG_LISTEN : the transport is listening for incoming
                                 connections/packets.
  • f_recv: REQUIRED function pointer used for receiving data
  • f_send: REQUIRED function pointer used for sending data
  • f_close: REQUIRED function pointer used to close transport socket
  • f_fmtaddr: REQUIRED function pointer to convert transport specific address to a printable string
  • f_open: OPTIONAL function pointer to open transport
  • f_accept: function pointer to accept an incoming connection (REQUIRED for local stream based transports)
  • f_config: OPTIONAL function pointer for providing additional configuration to a transport after it has been created.
  • f_copy: OPTIONAL function pointer to copy a transport specific items. This function is called from netsnmp_transport_copy() after all the transport data has been copied, allowing a specific transport to do any addition work needed to create a complete copy.
  • f_setup_session: OPTIONAL function pointer to allows transport to inspect/verify/modify session parameters.
  • base_transport: (New in 5.7) OPTIONAL pointer to another transport which will be used instead of or in addition to the functionality of the current transport.

Preparing for use

After a transport has been created, at some point it will be associated with a Net-SNMP session. If a f_config function is provided, it will be called when this association occurs. Then the f_open function, if provided, will be called. Finally, the f_setup_session, if provided, will be called.

f_config

The optional f_config function allows additional parameters to be passed to the transport, usually from the command line. For example, the two TLS transport use this to configure the certificates to be used for a connection. The function is called with the transport pointer and a token/value pair of strings.

A simplified example:

int
netsnmp_tlsbase_config(struct netsnmp_transport_s *t, const char *token,
                       const char *value) {
   _netsnmpTLSBaseData *tlsdata = t->data;

    if (strcmp(token, "localCert") == 0) {
        SNMP_FREE(tlsdata->our_identity);
        tlsdata->our_identity = strdup(value);
    }
    return SNMPERR_SUCCESS;
}


f_open

The optional f_open function performs any needed steps to open the transport. Often the transport is opened when it is created, and this function is not provided.

A greatly simplified example for a TCP based transport:

netsnmp_transport *
netsnmp_example_open(netsnmp_transport *t)
{
   netsnmp_udp_addr_pair *addr_pair = NULL;
   int rc = 0;

   addr_pair = (netsnmp_udp_addr_pair*)t->data;

   t->sock = socket(PF_INET, SOCK_STREAM, 0);
   if (t->sock < 0)
       return NULL;

   t->flags = NETSNMP_TRANSPORT_FLAG_STREAM;
   if (local) {
       t->flags |= NETSNMP_TRANSPORT_FLAG_LISTEN;
       rc = bind(t->sock, &addr_pair.sa, sizeof(struct sockaddr));
       if (rc != 0) {
           netsnmp_socketbase_close(t);
           return NULL;
       }

       rc = listen(t->sock, NETSNMP_STREAM_QUEUE_LEN);
       if (rc != 0) {
           netsnmp_socketbase_close(t);
           return NULL;
       }
   } else {
       rc = connect(t->sock, (struct sockaddr *)addr,
                    sizeof(struct sockaddr));

       if (rc < 0) {
           netsnmp_socketbase_close(t);
           return NULL;
       }
   }
   return t;
}

f_setup_session

The optional f_setup_session function allows the transport to inspect, verify or modify session parameters.

A simplified example from snmpTLSBaseDomain:

int
netsnmp_tlsbase_session_init(struct netsnmp_transport_s *transport,
                             struct snmp_session *sess)
{
   if (sess->securityModel == SNMP_DEFAULT_SECMODEL) {
       sess->securityModel = SNMP_SEC_MODEL_TSM;
   } else if (sess->securityModel != SNMP_SEC_MODEL_TSM) {
       sess->securityModel = SNMP_SEC_MODEL_TSM;
       snmp_log(LOG_ERR,
                "A non-TSM security model for (D)TLS; using TSM anyways\n");
   }

   if (sess->version != SNMP_VERSION_3) {
       sess->version = SNMP_VERSION_3;
       snmp_log(LOG_ERR,
                "SNMP version != 3 with (D)TLS; using 3 anyways\n");
   }

   if (sess->securityLevel <= 0)
       sess->securityLevel = SNMP_SEC_LEVEL_AUTHPRIV;

   return SNMPERR_SUCCESS;
}

Stream connections

f_accept

For stream based transports, the f_accept function is called to accept incoming connections.

A simplified example based on snmpTCPDomain:

static int
netsnmp_tcp_accept(netsnmp_transport *t)
{
   struct sockaddr *peer = NULL;
   netsnmp_udp_addr_pair *addr_pair = NULL;
   int             newsock = -1;
   socklen_t       peerlen = sizeof(netsnmp_udp_addr_pair);

   addr_pair = (netsnmp_udp_addr_pair *)malloc(peerlen);
   if (addr_pair == NULL)
       return -1;
   
   memset(addr_pair, 0, sizeof *addr_pair);
   peer = &addr_pair->remote_addr.sa;

   newsock = accept(t->sock, peer, &peerlen);
   if (newsock < 0)
       free(addr_pair);
   else {
       if (t->data != NULL)
           free(t->data);
       t->data = addr_pair;
       t->data_length = sizeof(netsnmp_udp_addr_pair);
   }

   return newsock;
}

Data passing functions

f_recv

The f_recv function is used to receive data from the transport. In addition to the transport pointer, this function takes a pointer to a buffer for the received data and the size of the buffer. The number of bytes received is returned by the function. Additionally a pointer to a pointer and a pointer to a integer are used to return a newly allocated opaque pointer and its length, which will be passed to the send function later. This is generally used to return the address from which the data was received, so that the reply goes to the same place.

A simplified example, based on snmpUDPBaseDomain:

int
netsnmp_udpbase_recv(netsnmp_transport *t, void *buf, int size,
                     void **opaque, int *olength)
{
    int             rc = -1;
    socklen_t       fromlen = sizeof(struct sockaddr);
    netsnmp_indexed_addr_pair *addr_pair = NULL;
    struct sockaddr *from;

    /*
     * allocate space for saving remote address
     */
    addr_pair = (netsnmp_indexed_addr_pair *)
                calloc(1, sizeof(netsnmp_indexed_addr_pair));
    if (addr_pair == NULL) {
        *opaque = NULL;
        *olength = 0;
        return -1;
    }
    from = &addr_pair->remote_addr.sa;

    while (rc < 0) {
        rc = recvfrom(t->sock, buf, size, NETSNMP_DONTWAIT, from, &fromlen);
        if (rc < 0 && errno != EINTR)
            break;
    }

    *opaque = (void *)addr_pair;
    *olength = sizeof(netsnmp_indexed_addr_pair);

    return rc;
}

f_send

The f_send function is used to send data to the transport. In addition to the transport pointer, this function takes a pointer to a buffer for the data to be sent and the size of the buffer. The number of bytes sent is returned by the function. Additionally a pointer to a pointer and a pointer to a integer are passed in. If it is not NULL, this opaque pointer was allocated by the f_receive function and should contain the address where the packet should be sent. Otherwise, the address should be in the transports data pointer.

A simplified example, based on snmpUDPBaseDomain:

int
netsnmp_udpbase_send(netsnmp_transport *t, void *buf, int size,
                     void **opaque, int *olength)
{
    int rc = -1;
    netsnmp_indexed_addr_pair *addr_pair = NULL;

    /*
     * find address to send to, from opaque pointer or t->data
     */
    if (opaque != NULL && *opaque != NULL &&
        *olength == sizeof(netsnmp_indexed_addr_pair)) {
        addr_pair = (netsnmp_indexed_addr_pair *) (*opaque);
    } else if (t != NULL && t->data != NULL &&
               t->data_length == sizeof(netsnmp_indexed_addr_pair))
       addr_pair = (netsnmp_indexed_addr_pair *) (t->data);

    if (addr_pair != NULL && t != NULL && t->sock >= 0) {
        struct sockaddr *to = &addr_pair->remote_addr.sa;
        while (rc < 0) {
            rc = sendto(t->sock, buf, size, 0, to, sizeof(struct sockaddr));
            if (rc < 0 && errno != EINTR)
		break;
       }
    }
    return rc;
}

Utility functions

f_close

The f_close function is used to close transport socket and release any resources that were allocated when the transport was created or opened.

A simplified example based on snmpTLSTCPDomain:

static int
netsnmp_tlstcp_close(netsnmp_transport *t)
{
   _netsnmpTLSBaseData *tlsdata = (_netsnmpTLSBaseData *)t->data;
   if (tlsdata->ssl)
       SSL_shutdown(tlsdata->ssl);

   netsnmp_tlsbase_free_tlsdata(tlsdata);

   t->data = NULL;
   return netsnmp_socketbase_close(t);
}

f_fmtaddr

The f_fmtaddr function converts the transport specific address to a printable string, generally for logging and debugging.

A simplified example from snmpIPv4BaseDomain:

char *
netsnmp_ipv4_fmtaddr(const char *prefix, netsnmp_transport *t,
                     void *data, int len)
{
   netsnmp_indexed_addr_pair *addr_pair = NULL;
   struct hostent *host;
   char tmp[64];

   if (data != NULL && len == sizeof(netsnmp_indexed_addr_pair))
       addr_pair = (netsnmp_indexed_addr_pair *) data;
   else if (t != NULL && t->data != NULL)
       addr_pair = (netsnmp_indexed_addr_pair *) t->data;

   if (addr_pair == NULL) {
       snprintf(tmp, sizeof(tmp), "%s: unknown", prefix);
   } else {
       struct sockaddr_in *to = NULL;
       to = (struct sockaddr_in *) &(addr_pair->remote_addr);
       if (to == NULL) {
           snprintf(tmp, sizeof(tmp), "%s: unknown->[%s]:%hu", prefix,
                    inet_ntoa(addr_pair->local_addr.sin.sin_addr),
                    ntohs(addr_pair->local_addr.sin.sin_port));
       } else if ( t && t->flags & NETSNMP_TRANSPORT_FLAG_HOSTNAME ) {
           host = gethostbyaddr((char *)&to->sin_addr, 4, AF_INET);
           return (host ? strdup(host->h_name) : NULL); 
       } else {
           snprintf(tmp, sizeof(tmp), "%s: [%s]:%hu->", prefix,
                    inet_ntoa(to->sin_addr), ntohs(to->sin_port));
           snprintf(tmp + strlen(tmp), sizeof(tmp)-strlen(tmp), "[%s]:%hu",
                    inet_ntoa(addr_pair->local_addr.sin.sin_addr),
                    ntohs(addr_pair->local_addr.sin.sin_port));
       }
   }
   tmp[sizeof(tmp)-1] = '\0';
   return strdup(tmp);
}

f_copy

The f_copy function copies transport specific items. This function is called from netsnmp_transport_copy() after all the transport data has been copied, allowing a specific transport to do any addition work needed to create a complete copy of itself.

A simplified example from snmpTLSTCPDomain:

static int
netsnmp_tlstcp_copy(netsnmp_transport *oldt, netsnmp_transport *newt)
{
   _netsnmpTLSBaseData *oldtlsdata = (_netsnmpTLSBaseData *) oldt->data;
   _netsnmpTLSBaseData *newtlsdata = (_netsnmpTLSBaseData *) newt->data;
   
   if (oldtlsdata->addr_string)
       newtlsdata->addr_string = strdup(oldtlsdata->addr_string);
   if (oldtlsdata->securityName)
       newtlsdata->securityName = strdup(oldtlsdata->securityName);
   if (oldtlsdata->addr)
       memdup((u_char**)&newtlsdata->addr, oldtlsdata->addr,
              sizeof(*oldtlsdata->addr));

   return 0;
}


Current Hierarchy

TBD

snmpUDPDomain

  • snmpUDPIPv4Base (udpipv4base_transport)
  • snmpIPv4Base (sockaddr_in, fmtaddr)
  • snmpUDPBase (_recv, _send, _recvfrom, _sendto)

snmpTCPDomain

  • snmpIPv4Base
  • snmpSocketBase (close)
  • snmpTCPBase (recv, send)