From 9f4eb5cc7f6fe38ed968aade9f46112566270592 Mon Sep 17 00:00:00 2001 From: Remi Gacogne Date: Mon, 5 Dec 2016 15:59:58 +0100 Subject: [PATCH] dnsdist: Add SNMP support --- build-scripts/travis.sh | 10 +- docs/MIBS/DNSDIST-MIB.txt | 710 ++++++++++++++++++++++ m4/pdns_with_net_snmp.m4 | 23 + pdns/README-dnsdist.md | 64 ++ pdns/dnsdist-console.cc | 4 + pdns/dnsdist-lua.cc | 23 +- pdns/dnsdist-lua2.cc | 46 ++ pdns/dnsdist-snmp.cc | 600 ++++++++++++++++++ pdns/dnsdist-snmp.hh | 21 + pdns/dnsdist.cc | 12 +- pdns/dnsdist.hh | 17 + pdns/dnsdistdist/DNSDIST-MIB.txt | 1 + pdns/dnsdistdist/Makefile.am | 10 +- pdns/dnsdistdist/configure.ac | 5 + pdns/dnsdistdist/dnsdist-snmp.cc | 1 + pdns/dnsdistdist/dnsdist-snmp.hh | 1 + pdns/dnsdistdist/m4/pdns_with_net_snmp.m4 | 1 + pdns/dnsdistdist/snmp-agent.cc | 1 + pdns/dnsdistdist/snmp-agent.hh | 1 + pdns/dnsrulactions.hh | 44 ++ pdns/snmp-agent.cc | 121 ++++ pdns/snmp-agent.hh | 59 ++ regression-tests.dnsdist/requirements.txt | 1 + regression-tests.dnsdist/snmpd.conf | 13 + regression-tests.dnsdist/test_SNMP.py | 148 +++++ 25 files changed, 1924 insertions(+), 13 deletions(-) create mode 100644 docs/MIBS/DNSDIST-MIB.txt create mode 100644 m4/pdns_with_net_snmp.m4 create mode 100644 pdns/dnsdist-snmp.cc create mode 100644 pdns/dnsdist-snmp.hh create mode 120000 pdns/dnsdistdist/DNSDIST-MIB.txt create mode 120000 pdns/dnsdistdist/dnsdist-snmp.cc create mode 120000 pdns/dnsdistdist/dnsdist-snmp.hh create mode 120000 pdns/dnsdistdist/m4/pdns_with_net_snmp.m4 create mode 120000 pdns/dnsdistdist/snmp-agent.cc create mode 120000 pdns/dnsdistdist/snmp-agent.hh create mode 100644 pdns/snmp-agent.cc create mode 100644 pdns/snmp-agent.hh create mode 100644 regression-tests.dnsdist/snmpd.conf create mode 100644 regression-tests.dnsdist/test_SNMP.py diff --git a/build-scripts/travis.sh b/build-scripts/travis.sh index c4694d01f9..630da302b5 100755 --- a/build-scripts/travis.sh +++ b/build-scripts/travis.sh @@ -357,7 +357,15 @@ install_recursor() { } install_dnsdist() { - printf "" + # recursor test requirements / setup + run "sudo apt-get -qq --no-install-recommends install \ + snmpd \ + libsnmp-dev" + run "sudo sed -i \"s/agentxperms 0700 0755 dnsdist/agentxperms 0700 0755 ${USER}/g\" regression-tests.dnsdist/snmpd.conf" + run "sudo cp -f regression-tests.dnsdist/snmpd.conf /etc/snmp/snmpd.conf" + run "sudo service snmpd restart" + # fun story, the directory perms are only applied if it doesn't exist yet, and it is created by the init script, so.. + run "sudo chmod 0755 /var/agentx" } build_auth() { diff --git a/docs/MIBS/DNSDIST-MIB.txt b/docs/MIBS/DNSDIST-MIB.txt new file mode 100644 index 0000000000..7eba714ac0 --- /dev/null +++ b/docs/MIBS/DNSDIST-MIB.txt @@ -0,0 +1,710 @@ +-- -*- snmpv2 -*- +-- ---------------------------------------------------------------------- +-- MIB file for dnsdist +-- ---------------------------------------------------------------------- + +DNSDIST-MIB DEFINITIONS ::= BEGIN + +IMPORTS + OBJECT-TYPE, MODULE-IDENTITY, enterprises, + Counter64, Unsigned32, NOTIFICATION-TYPE + FROM SNMPv2-SMI + CounterBasedGauge64 + FROM HCNUM-TC + Float64TC + FROM FLOAT-TC-MIB + OBJECT-GROUP, MODULE-COMPLIANCE, NOTIFICATION-GROUP + FROM SNMPv2-CONF + InetAddressType + FROM INET-ADDRESS-MIB + TEXTUAL-CONVENTION, DisplayString + FROM SNMPv2-TC; + +dnsdist MODULE-IDENTITY + LAST-UPDATED "201611080000Z" + ORGANIZATION "PowerDNS BV" + CONTACT-INFO "support@powerdns.com" + DESCRIPTION + "This MIB module describes information gathered through dnsdist." + + REVISION "201611080000Z" + DESCRIPTION "Initial revision." + + ::= { powerdns 3 } + +powerdns OBJECT IDENTIFIER ::= { enterprises 43315 } + +stats OBJECT IDENTIFIER ::= { dnsdist 1 } + +queries OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of queries received" + ::= { stats 1 } + +responses OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of responses received" + ::= { stats 2 } + +servfailResponses OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of servfail responses received" + ::= { stats 3 } + +aclDrops OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of queries dropped because of the ACL" + ::= { stats 4 } + +blockFilters OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of queries dropped because of the block filters" + ::= { stats 5 } + +ruleDrop OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of queries dropped because of a rule" + ::= { stats 6 } + +ruleNXDomain OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of NXDomain responses returned because of a rule" + ::= { stats 7 } + +ruleRefused OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of Refused responses returned because of a rule" + ::= { stats 8 } + +selfAnswered OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of self-answered responses" + ::= { stats 9 } + +downstreamTimeouts OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of downstream timeouts" + ::= { stats 10 } + +downstreamSendErrors OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of downstream send errors" + ::= { stats 11 } + +truncFailures OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of errors while truncating a response" + ::= { stats 12 } + +noPolicy OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of queries dropped because no server was available" + ::= { stats 13 } + +latency01 OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of queries answered in less than 1 ms" + ::= { stats 14 } + +latency110 OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of queries answered in 1-10 ms" + ::= { stats 15 } + +latency1050 OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of queries answered in 10-50 ms" + ::= { stats 16 } + +latency50100 OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of queries answered in 50-100 ms" + ::= { stats 17 } + +latency1001000 OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of queries answered in 100-1000 ms" + ::= { stats 18 } + +latencySlow OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of queries answered in more than 1s" + ::= { stats 19 } + +latencyAVG100 OBJECT-TYPE + SYNTAX Float64TC + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Average latency over the last 100 queries" + ::= { stats 20 } + +latencyAVG1000 OBJECT-TYPE + SYNTAX Float64TC + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Average latency over the last 1000 queries" + ::= { stats 21 } + +latencyAVG10000 OBJECT-TYPE + SYNTAX Float64TC + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Average latency over the last 10000 queries" + ::= { stats 22 } + +latencyAVG1000000 OBJECT-TYPE + SYNTAX Float64TC + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Average latency over the last 1000000 queries" + ::= { stats 23 } + +uptime OBJECT-TYPE + SYNTAX CounterBasedGauge64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Uptime of the dnsdist process, in seconds" + ::= { stats 24 } + +realMemoryUsage OBJECT-TYPE + SYNTAX CounterBasedGauge64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Memory usage" + ::= { stats 25 } + +nonCompliantQueries OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of queries dropped as non-compliant" + ::= { stats 26 } + +nonCompliantResponses OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of responses dropped as non-compliant" + ::= { stats 27 } + +rdQueries OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of queries with the RD flag set" + ::= { stats 28 } + +emptyQueries OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of empty queries received" + ::= { stats 29 } + +cacheHits OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of cache hits" + ::= { stats 30 } + +cacheMisses OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of cache misses" + ::= { stats 31 } + +cpuUserMSec OBJECT-TYPE + SYNTAX CounterBasedGauge64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "CPU Usage (user)" + ::= { stats 32 } + +cpuSysMSec OBJECT-TYPE + SYNTAX CounterBasedGauge64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "CPU Usage (sys)" + ::= { stats 33 } + +fdUsage OBJECT-TYPE + SYNTAX CounterBasedGauge64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of file descriptors" + ::= { stats 34 } + +dynBlocked OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of queries dropped because of a dynamic block" + ::= { stats 35 } + +dynBlockNMGSize OBJECT-TYPE + SYNTAX CounterBasedGauge64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Dynamic blocks (NMG) size" + ::= { stats 36 } + +backendStatTable OBJECT-TYPE + SYNTAX SEQUENCE OF BackendStatEntry + MAX-ACCESS not-accessible + STATUS current + DESCRIPTION "Statistics for backends" + ::= { dnsdist 2 } + +backendStatEntry OBJECT-TYPE + SYNTAX BackendStatEntry + MAX-ACCESS not-accessible + STATUS current + DESCRIPTION "Statistics for one backend" + INDEX { backendId } + ::= { backendStatTable 1 } + +BackendStatEntry ::= SEQUENCE { + backendId Unsigned32, + backendName DisplayString, + backendLatency CounterBasedGauge64, + backendWeight CounterBasedGauge64, + backendOutstanding CounterBasedGauge64, + backendQPSLimit CounterBasedGauge64, + backendReused Counter64, + backendState DisplayString, + backendAddress OCTET STRING, + backendPools DisplayString, + backendQPS CounterBasedGauge64, + backendQueries Counter64, + backendOrder CounterBasedGauge64 +} + +backendId OBJECT-TYPE + SYNTAX Unsigned32 + MAX-ACCESS not-accessible + STATUS current + DESCRIPTION + "Backend ID" + ::= { backendStatEntry 1 } + +backendName OBJECT-TYPE + SYNTAX DisplayString + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Backend name" + ::= { backendStatEntry 2 } + +backendLatency OBJECT-TYPE + SYNTAX CounterBasedGauge64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Backend latency" + ::= { backendStatEntry 3 } + +backendWeight OBJECT-TYPE + SYNTAX CounterBasedGauge64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Backend weight" + ::= { backendStatEntry 4 } + +backendOutstanding OBJECT-TYPE + SYNTAX CounterBasedGauge64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Backend outstanding queries" + ::= { backendStatEntry 5 } + +backendQPSLimit OBJECT-TYPE + SYNTAX CounterBasedGauge64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Backend QPS limit" + ::= { backendStatEntry 6 } + +backendReused OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Backend reused slots" + ::= { backendStatEntry 7 } + +backendState OBJECT-TYPE + SYNTAX DisplayString + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Backend state" + ::= { backendStatEntry 8 } + +backendAddress OBJECT-TYPE + SYNTAX OCTET STRING (SIZE (2..24)) + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Backend address" + ::= { backendStatEntry 9 } + +backendPools OBJECT-TYPE + SYNTAX DisplayString + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "List of pools this backend belongs to" + ::= { backendStatEntry 10 } + +backendQPS OBJECT-TYPE + SYNTAX CounterBasedGauge64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Backend QPS" + ::= { backendStatEntry 11 } + +backendQueries OBJECT-TYPE + SYNTAX Counter64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of queries sent to this backend" + ::= { backendStatEntry 12 } + +backendOrder OBJECT-TYPE + SYNTAX CounterBasedGauge64 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Backend order" + ::= { backendStatEntry 13 } + +--- +--- Textual Conventions +--- + +SocketProtocolType ::= TEXTUAL-CONVENTION + STATUS current + DESCRIPTION + "A value that represents a type of socket protocol." + SYNTAX INTEGER { + unknown(0), + udp(1), + tcp(2) + } + +DNSQueryType ::= TEXTUAL-CONVENTION + STATUS current + DESCRIPTION + "A value that represents a type of DNS query (question or response)." + SYNTAX INTEGER { + unknown(0), + question(1), + response(2) + } + +--- +--- Traps / Notifications +--- + +trap OBJECT IDENTIFIER ::= { dnsdist 10 } +traps OBJECT IDENTIFIER ::= { trap 0 } --- reverse-mappable +trapObjects OBJECT IDENTIFIER ::= { dnsdist 11 } + +socketFamily OBJECT-TYPE + SYNTAX InetAddressType + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Socket family type" + ::= { trapObjects 1 } + +socketProtocol OBJECT-TYPE + SYNTAX SocketProtocolType + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Socket protocol type" + ::= { trapObjects 2 } + +fromAddress OBJECT-TYPE + SYNTAX OCTET STRING (SIZE (2..24)) + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Requestor address" + ::= { trapObjects 3 } + +toAddress OBJECT-TYPE + SYNTAX OCTET STRING (SIZE (2..24)) + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Responder address" + ::= { trapObjects 4 } + +queryType OBJECT-TYPE + SYNTAX DNSQueryType + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Query / Response" + ::= { trapObjects 5 } + +querySize OBJECT-TYPE + SYNTAX Unsigned32 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Size in bytes" + ::= { trapObjects 6 } + +queryID OBJECT-TYPE + SYNTAX Unsigned32 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "DNS query ID" + ::= { trapObjects 7 } + +qName OBJECT-TYPE + SYNTAX OCTET STRING (SIZE (0..255)) + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "DNS qname" + ::= { trapObjects 8 } + +qClass OBJECT-TYPE + SYNTAX Unsigned32 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "DNS query class" + ::= { trapObjects 9 } + +qType OBJECT-TYPE + SYNTAX Unsigned32 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "DNS query type" + ::= { trapObjects 10 } + +trapReason OBJECT-TYPE + SYNTAX OCTET STRING + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Reason for this trap" + ::= { trapObjects 11 } + +backendStatusChangeTrap NOTIFICATION-TYPE + OBJECTS { + backendName, + backendAddress, + backendState + } + STATUS current + DESCRIPTION "Backend status changed" + ::= { traps 1 } + +actionTrap NOTIFICATION-TYPE + OBJECTS { + socketFamily, + socketProtocol, + fromAddress, + toAddress, + queryType, + querySize, + queryID, + qName, + qClass, + qType, + trapReason + } + STATUS current + DESCRIPTION "Trap sent by SNMPTrapAction" + ::= { traps 2 } + +customTrap NOTIFICATION-TYPE + OBJECTS { + trapReason + } + STATUS current + DESCRIPTION "Trap sent by sendCustomTrap" + ::= { traps 3 } + +--- +--- Conformance +--- + +dnsdistConformance OBJECT IDENTIFIER ::= { dnsdist 100 } + +dnsdistCompliances MODULE-COMPLIANCE + STATUS current + DESCRIPTION "dnsdist compliance statement" + MODULE + MANDATORY-GROUPS { + dnsdistGroup, + dnsdistTrapsGroup + } + ::= { dnsdistConformance 1 } + +dnsdistGroup OBJECT-GROUP + OBJECTS { + queries, + responses, + servfailResponses, + aclDrops, + blockFilters, + ruleDrop, + ruleNXDomain, + ruleRefused, + selfAnswered, + downstreamTimeouts, + downstreamSendErrors, + truncFailures, + noPolicy, + latency01, + latency110, + latency1050, + latency50100, + latency1001000, + latencySlow, + latencyAVG100, + latencyAVG1000, + latencyAVG10000, + latencyAVG1000000, + uptime, + realMemoryUsage, + nonCompliantQueries, + nonCompliantResponses, + rdQueries, + emptyQueries, + cacheHits, + cacheMisses, + cpuUserMSec, + cpuSysMSec, + fdUsage, + dynBlocked, + dynBlockNMGSize, + backendName, + backendLatency, + backendWeight, + backendOutstanding, + backendQPSLimit, + backendReused, + backendState, + backendAddress, + backendPools, + backendQPS, + backendQueries, + backendOrder, + socketFamily, + socketProtocol, + fromAddress, + toAddress, + queryType, + querySize, + queryID, + qName, + qClass, + qType, + trapReason + } + STATUS current + DESCRIPTION "Objects conformance group for dnsdist" + ::= { dnsdistConformance 2 } + +dnsdistTrapsGroup NOTIFICATION-GROUP + NOTIFICATIONS { + actionTrap, + customTrap, + backendStatusChangeTrap + } + STATUS current + DESCRIPTION "Traps conformance group for dnsdist" + ::= { dnsdistConformance 3 } + +END diff --git a/m4/pdns_with_net_snmp.m4 b/m4/pdns_with_net_snmp.m4 new file mode 100644 index 0000000000..4134fc454e --- /dev/null +++ b/m4/pdns_with_net_snmp.m4 @@ -0,0 +1,23 @@ +AC_DEFUN([PDNS_WITH_NET_SNMP], [ + AC_MSG_CHECKING([if we need to link in Net SNMP]) + AC_ARG_WITH([net-snmp], + AS_HELP_STRING([--with-net-snmp],[enable net snmp support @<:@default=auto@:>@]), + [with_net_snmp=$withval], + [with_net_snmp=auto], + ) + AC_MSG_RESULT([$with_net_snmp]) + + AS_IF([test "x$with_net_snmp" != "xno"], [ + AS_IF([test "x$with_net_snmp" = "xyes" -o "x$with_net_snmp" = "xauto"], [ + AC_CHECK_PROG([NET_SNMP_CFLAGS], [net-snmp-config], [`net-snmp-config --cflags`]) + AC_CHECK_PROG([NET_SNMP_LIBS], [net-snmp-config], [`net-snmp-config --agent-libs`]) + ]) + ]) + AS_IF([test "x$with_net_snmp" = "xyes"], [ + AS_IF([test x"$NET_SNMP_LIBS" = "x"], [ + AC_MSG_ERROR([Net SNMP requested but libraries were not found]) + ]) + ]) + AM_CONDITIONAL([HAVE_NET_SNMP], [test x"$NET_SNMP_LIBS" != "x"]) + AS_IF([test x"$NET_SNMP_LIBS" != "x"], [AC_DEFINE([HAVE_NET_SNMP], [1], [Define if using Net SNMP.])]) +]) diff --git a/pdns/README-dnsdist.md b/pdns/README-dnsdist.md index c41f94542d..5e69288d53 100644 --- a/pdns/README-dnsdist.md +++ b/pdns/README-dnsdist.md @@ -376,6 +376,7 @@ Current actions are: * Skip the cache, if any * Log query content to a remote server (RemoteLogAction) * Alter the EDNS Client Subnet parameters (DisableECSAction, ECSOverrideAction, ECSPrefixLengthAction) + * Send an SNMP trap (SNMPTrapAction) Current response actions are: @@ -383,6 +384,7 @@ Current response actions are: * Delay a response by n milliseconds (DelayResponseAction), over UDP only * Drop (DropResponseAction) * Log response content to a remote server (RemoteLogResponseAction) + * Send an SNMP trap (SNMPTrapResponseAction) Rules can be added via: @@ -443,6 +445,7 @@ Some specific actions do not stop the processing when they match, contrary to al * Log * MacAddr * No Recurse + * SNMP Trap * and of course None A convenience function `makeRule()` is supplied which will make a NetmaskGroupRule for you or a SuffixMatchNodeRule @@ -1229,6 +1232,61 @@ over the past 10 seconds, and the dynamic block will last for 60 seconds. This feature has been successfully tested on Arch Linux, Arch Linux ARM, Fedora Core 23 and Ubuntu Xenial. +SNMP support +------------ +`dnsdist` supports exporting statistics and sending traps over SNMP when compiled +with `Net SNMP` support, acting as an `AgentX` subagent. +`SNMP` support is enabled via the `snmpAgent(enableTraps [, masterSocket])` directive, +where `enableTraps` is a boolean indicating whether traps should be sent and `masterSocket` +is an optional string specifying how to connect to the master agent. The default for this +last parameter is to use an Unix socket, but others options are available, such as TCP: `tcp:localhost:705` + +By default, the only traps sent when `enableTraps` is set to `true` are backend status change notifications, but traps can also be sent: + + * from Lua, with `sendCustomTrap(string)` and `dq:sendTrap(string)` + * for selected queries and responses, using `SNMPTrapAction([string])` and `SNMPTrapResponseAction([string])` + +`Net SNMP snmpd` doesn't accept subagent connections by default, so to use the `SNMP` +features of `dnsdist` the following line should be added to the `snmpd.conf` configuration +file: + +``` +master agentx +``` + +In addition to that, the permissions on the resulting socket might need to be adjusted +so that the `dnsdist` user can write to it. This can be done with the following lines in +`snmpd.conf` (assuming `dnsdist` is running as `dnsdist:dnsdist`): + +``` +agentxperms 0700 0700 dnsdist dnsdist +``` + +In order to allow the retrieval of statistics via `SNMP`, `snmpd`'s access control +has to configured. A very simple `SNMPv2c` setup only needs the configuration of +a read-only community in `snmpd.conf`: + +``` +rocommunity dnsdist42 +``` + +`snmpd` also supports more secure `SNMPv3` setup, using for example the `createUser` and +`rouser` directives: + +``` +createUser myuser SHA "my auth key" AES "my enc key" +rouser myuser +``` + +`snmpd` can be instructed to send `SNMPv2` traps to a remote `SNMP` trap receiver by adding the +following directive to the `snmpd.conf` configuration file: + +``` +trap2sink 192.0.2.1 +``` + +The description of `dnsdist`'s `SNMP MIB` is available in `DNSDIST-MIB.txt`. + All functions and types ----------------------- Within `dnsdist` several core object types exist: @@ -1395,6 +1453,8 @@ instantiate a server with additional parameters * `RemoteLogAction(RemoteLogger [, alterFunction])`: send the content of this query to a remote logger via Protocol Buffer. `alterFunction` is a callback, receiving a DNSQuestion and a DNSDistProtoBufMessage, that can be used to modify the Protocol Buffer content, for example for anonymization purposes * `RemoteLogResponseAction(RemoteLogger [,alterFunction [,includeCNAME]])`: send the content of this response to a remote logger via Protocol Buffer. `alterFunction` is the same callback than the one in `RemoteLogAction` and `includeCNAME` indicates whether CNAME records inside the response should be parsed and exported. The default is to only exports A and AAAA records * `SkipCacheAction()`: don't lookup the cache for this query, don't store the answer + * `SNMPTrapAction([reason])`: send an SNMP trap, adding the optional `reason` string as the query description + * `SNMPTrapResponseAction([reason])`: send an SNMP trap, adding the optional `reason` string as the response description * `SpoofAction(ip[, ip])` or `SpoofAction({ip, ip, ..}): forge a response with the specified IPv4 (for an A query) or IPv6 (for an AAAA). If you specify multiple addresses, all that match the query type (A, AAAA or ANY) will get spoofed in * `SpoofCNAMEAction(cname)`: forge a response with the specified CNAME value * `TCAction()`: create answer to query with TC and RD bits set, to move to TCP @@ -1507,6 +1567,7 @@ instantiate a server with additional parameters * member `qtype`: QType (as an unsigned integer) of this question * member `remoteaddr`: ComboAddress of the remote client * member `rcode`: RCode of this question + * member `sendTrap([reason])`: send a trap containing the description of the query, and the optional `reason` string * member `size`: the total size of the buffer starting at `dh` * member `skipCache`: whether to skip cache lookup / storing the answer for this question (settable) * member `tcp`: whether this question was received over a TCP socket @@ -1584,6 +1645,9 @@ instantiate a server with additional parameters * function `unregisterDynBPFFilter(DynBPFFilter)`: unregister this dynamic BPF filter * RemoteLogger related: * `newRemoteLogger(address:port [, timeout=2, maxQueuedEntries=100, reconnectWaitTime=1])`: create a Remote Logger object, to use with `RemoteLogAction()` and `RemoteLogResponseAction()` + * SNMP related: + * `snmpAgent(enableTraps [, masterSocket])`: enable `SNMP` support. `enableTraps` is a boolean indicating whether traps should be sent and `masterSocket` an optional string specifying how to connect to the master agent + * `sendCustomTrap(str)`: send a custom `SNMP` trap from Lua, containing the `str` string All hooks --------- diff --git a/pdns/dnsdist-console.cc b/pdns/dnsdist-console.cc index f4dec4ad51..c54d2f6c19 100644 --- a/pdns/dnsdist-console.cc +++ b/pdns/dnsdist-console.cc @@ -352,6 +352,7 @@ const std::vector g_consoleKeywords{ { "QNameWireLengthRule", true, "min, max", "matches if the qname's length on the wire is less than `min` or more than `max` bytes" }, { "QTypeRule", true, "qtype", "matches queries with the specified qtype" }, { "RCodeRule", true, "rcode", "matches responses with the specified rcode" }, + { "sendCustomTrap", true, "str", "send a custom `SNMP` trap from Lua, containing the `str` string"}, { "setACL", true, "{netmask, netmask}", "replace the ACL set with these netmasks. Use `setACL({})` to reset the list, meaning no one can use us" }, { "setAPIWritable", true, "bool, dir", "allow modifications via the API. if `dir` is set, it must be a valid directory where the configuration files will be written by the API" }, { "setDNSSECPool", true, "pool name", "move queries requesting DNSSEC processing to this pool" }, @@ -391,6 +392,9 @@ const std::vector g_consoleKeywords{ { "showTCPStats", true, "", "show some statistics regarding TCP" }, { "showVersion", true, "", "show the current version" }, { "shutdown", true, "", "shut down `dnsdist`" }, + { "snmpAgent", true, "enableTraps [, masterSocket]", "enable `SNMP` support. `enableTraps` is a boolean indicating whether traps should be sent and `masterSocket` an optional string specifying how to connect to the master agent"}, + { "SNMPTrapAction", true, "[reason]", "send an SNMP trap, adding the optional `reason` string as the query description"}, + { "SNMPTrapResponseAction", true, "[reason]", "send an SNMP trap, adding the optional `reason` string as the response description"}, { "SpoofAction", true, "{ip, ...} ", "forge a response with the specified IPv4 (for an A query) or IPv6 (for an AAAA). If you specify multiple addresses, all that match the query type (A, AAAA or ANY) will get spoofed in" }, { "TCAction", true, "", "create answer to query with TC and RD bits set, to move to TCP" }, { "testCrypto", true, "", "test of the crypto all works" }, diff --git a/pdns/dnsdist-lua.cc b/pdns/dnsdist-lua.cc index b6687597d1..0b3867665a 100644 --- a/pdns/dnsdist-lua.cc +++ b/pdns/dnsdist-lua.cc @@ -679,14 +679,7 @@ vector> setupLua(bool client, const std::string& confi int counter=0; auto states = g_dstates.getCopy(); for(const auto& s : states) { - string status; - if(s->availability == DownstreamState::Availability::Up) - status = "UP"; - else if(s->availability == DownstreamState::Availability::Down) - status = "DOWN"; - else - status = (s->upStatus ? "up" : "down"); - + string status = s->getStatus(); string pools; for(auto& p : s->pools) { if(!pools.empty()) @@ -1542,6 +1535,13 @@ vector> setupLua(bool client, const std::string& confi g_lua.registerFunction("getDO", [](const DNSQuestion& dq) { return getEDNSZ((const char*)dq.dh, dq.len) & EDNS_HEADER_FLAG_DO; }); + g_lua.registerFunction("sendTrap", [](const DNSQuestion& dq, boost::optional reason) { +#ifdef HAVE_NET_SNMP + if (g_snmpAgent && g_snmpTrapsEnabled) { + g_snmpAgent->sendDNSTrap(dq, reason ? *reason : ""); + } +#endif /* HAVE_NET_SNMP */ + }); /* LuaWrapper doesn't support inheritance */ g_lua.registerMember("localaddr", [](const DNSResponse& dq) -> const ComboAddress { return *dq.local; }, [](DNSResponse& dq, const ComboAddress newLocal) { (void) newLocal; }); @@ -1559,6 +1559,13 @@ vector> setupLua(bool client, const std::string& confi g_lua.registerFunction editFunc)>("editTTLs", [](const DNSResponse& dr, std::function editFunc) { editDNSPacketTTL((char*) dr.dh, dr.len, editFunc); }); + g_lua.registerFunction("sendTrap", [](const DNSResponse& dr, boost::optional reason) { +#ifdef HAVE_NET_SNMP + if (g_snmpAgent && g_snmpTrapsEnabled) { + g_snmpAgent->sendDNSTrap(dr, reason ? *reason : ""); + } +#endif /* HAVE_NET_SNMP */ + }); g_lua.writeFunction("setMaxTCPClientThreads", [](uint64_t max) { if (!g_configurationDone) { diff --git a/pdns/dnsdist-lua2.cc b/pdns/dnsdist-lua2.cc index 1455b5a734..25708a0158 100644 --- a/pdns/dnsdist-lua2.cc +++ b/pdns/dnsdist-lua2.cc @@ -1245,4 +1245,50 @@ void moreLua(bool client) g_useTCPSinglePipe = flag; }); + g_lua.writeFunction("snmpAgent", [](bool enableTraps, boost::optional masterSocket) { +#ifdef HAVE_NET_SNMP + if (g_configurationDone) { + errlog("snmpAgent() cannot be used at runtime!"); + g_outputBuffer="snmpAgent() cannot be used at runtime!\n"; + return; + } + + if (g_snmpEnabled) { + errlog("snmpAgent() cannot be used twice!"); + g_outputBuffer="snmpAgent() cannot be used twice!\n"; + return; + } + + g_snmpEnabled = true; + g_snmpTrapsEnabled = enableTraps; + g_snmpAgent = new DNSDistSNMPAgent("dnsdist", masterSocket ? *masterSocket : std::string()); +#else + errlog("NET SNMP support is required to use snmpAgent()"); + g_outputBuffer="NET SNMP support is required to use snmpAgent()\n"; +#endif /* HAVE_NET_SNMP */ + }); + + g_lua.writeFunction("SNMPTrapAction", [](boost::optional reason) { +#ifdef HAVE_NET_SNMP + return std::shared_ptr(new SNMPTrapAction(reason ? *reason : "")); +#else + throw std::runtime_error("NET SNMP support is required to use SNMPTrapAction()"); +#endif /* HAVE_NET_SNMP */ + }); + + g_lua.writeFunction("SNMPTrapResponseAction", [](boost::optional reason) { +#ifdef HAVE_NET_SNMP + return std::shared_ptr(new SNMPTrapResponseAction(reason ? *reason : "")); +#else + throw std::runtime_error("NET SNMP support is required to use SNMPTrapResponseAction()"); +#endif /* HAVE_NET_SNMP */ + }); + + g_lua.writeFunction("sendCustomTrap", [](const std::string& str) { +#ifdef HAVE_NET_SNMP + if (g_snmpAgent && g_snmpTrapsEnabled) { + g_snmpAgent->sendCustomTrap(str); + } +#endif /* HAVE_NET_SNMP */ + }); } diff --git a/pdns/dnsdist-snmp.cc b/pdns/dnsdist-snmp.cc new file mode 100644 index 0000000000..e3528eb33f --- /dev/null +++ b/pdns/dnsdist-snmp.cc @@ -0,0 +1,600 @@ + +#include "dnsdist-snmp.hh" +#include "dolog.hh" + +#ifdef HAVE_NET_SNMP + +#define DNSDIST_OID 1, 3, 6, 1, 4, 1, 43315, 3 +#define DNSDIST_STATS_OID DNSDIST_OID, 1 +#define DNSDIST_STATS_TABLE_OID DNSDIST_OID, 2 +#define DNSDIST_TRAPS_OID DNSDIST_OID, 10, 0 +#define DNSDIST_TRAP_OBJECTS_OID DNSDIST_OID, 11 + +static const oid queriesOID[] = { DNSDIST_STATS_OID, 1 }; +static const oid responsesOID[] = { DNSDIST_STATS_OID, 2 }; +static const oid servfailResponsesOID[] = { DNSDIST_STATS_OID, 3 }; +static const oid aclDropsOID[] = { DNSDIST_STATS_OID, 4 }; +static const oid blockFilterOID[] = { DNSDIST_STATS_OID, 5 }; +static const oid ruleDropOID[] = { DNSDIST_STATS_OID, 6 }; +static const oid ruleNXDomainOID[] = { DNSDIST_STATS_OID, 7 }; +static const oid ruleRefusedOID[] = { DNSDIST_STATS_OID, 8 }; +static const oid selfAnsweredOID[] = { DNSDIST_STATS_OID, 9 }; +static const oid downstreamTimeoutsOID[] = { DNSDIST_STATS_OID, 10 }; +static const oid downstreamSendErrorsOID[] = { DNSDIST_STATS_OID, 11 }; +static const oid truncFailOID[] = { DNSDIST_STATS_OID, 12 }; +static const oid noPolicyOID[] = { DNSDIST_STATS_OID, 13 }; +static const oid latency0_1OID[] = { DNSDIST_STATS_OID, 14 }; +static const oid latency1_10OID[] = { DNSDIST_STATS_OID, 15 }; +static const oid latency10_50OID[] = { DNSDIST_STATS_OID, 16 }; +static const oid latency50_100OID[] = { DNSDIST_STATS_OID, 17 }; +static const oid latency100_1000OID[] = { DNSDIST_STATS_OID, 18 }; +static const oid latencySlowOID[] = { DNSDIST_STATS_OID, 19 }; +static const oid latencyAvg100OID[] = { DNSDIST_STATS_OID, 20 }; +static const oid latencyAvg1000OID[] = { DNSDIST_STATS_OID, 21 }; +static const oid latencyAvg10000OID[] = { DNSDIST_STATS_OID, 22 }; +static const oid latencyAvg1000000OID[] = { DNSDIST_STATS_OID, 23 }; +static const oid uptimeOID[] = { DNSDIST_STATS_OID, 24 }; +static const oid realMemoryUsageOID[] = { DNSDIST_STATS_OID, 25 }; +static const oid nonCompliantQueriesOID[] = { DNSDIST_STATS_OID, 26 }; +static const oid nonCompliantResponsesOID[] = { DNSDIST_STATS_OID, 27 }; +static const oid rdQueriesOID[] = { DNSDIST_STATS_OID, 28 }; +static const oid emptyQueriesOID[] = { DNSDIST_STATS_OID, 29 }; +static const oid cacheHitsOID[] = { DNSDIST_STATS_OID, 30 }; +static const oid cacheMissesOID[] = { DNSDIST_STATS_OID, 31 }; +static const oid cpuUserMSecOID[] = { DNSDIST_STATS_OID, 32 }; +static const oid cpuSysMSecOID[] = { DNSDIST_STATS_OID, 33 }; +static const oid fdUsageOID[] = { DNSDIST_STATS_OID, 34 }; +static const oid dynBlockedOID[] = { DNSDIST_STATS_OID, 35 }; +static const oid dynBlockedNMGSizeOID[] = { DNSDIST_STATS_OID, 36 }; + +static std::unordered_map s_statsMap; + +/* We are never called for a GETNEXT if it's registered as a + "instance", as it's "magically" handled for us. */ +/* a instance handler also only hands us one request at a time, so + we don't need to loop over a list of requests; we'll only get one. */ + +static int handleCounter64Stats(netsnmp_mib_handler* handler, + netsnmp_handler_registration* reginfo, + netsnmp_agent_request_info* reqinfo, + netsnmp_request_info* requests) +{ + if (reqinfo->mode != MODE_GET) { + return SNMP_ERR_GENERR; + } + + if (reginfo->rootoid_len != OID_LENGTH(queriesOID) + 1) { + return SNMP_ERR_GENERR; + } + + const auto& it = s_statsMap.find(reginfo->rootoid[reginfo->rootoid_len - 2]); + if (it == s_statsMap.end()) { + return SNMP_ERR_GENERR; + } + + if (const auto& val = boost::get(&it->second)) { + return DNSDistSNMPAgent::setCounter64Value(requests, (*val)->load()); + } + + return SNMP_ERR_GENERR; +} + +static void registerCounter64Stat(const char* name, const oid statOID[], size_t statOIDLength, std::atomic* ptr) +{ + if (statOIDLength != OID_LENGTH(queriesOID)) { + errlog("Invalid OID for SNMP Counter64 statistic %s", name); + return; + } + + if (s_statsMap.find(statOID[statOIDLength - 1]) != s_statsMap.end()) { + errlog("OID for SNMP Counter64 statistic %s has already been registered", name); + return; + } + + s_statsMap[statOID[statOIDLength - 1]] = ptr; + netsnmp_register_scalar(netsnmp_create_handler_registration(name, + handleCounter64Stats, + statOID, + statOIDLength, + HANDLER_CAN_RONLY)); +} + +static int handleFloatStats(netsnmp_mib_handler* handler, + netsnmp_handler_registration* reginfo, + netsnmp_agent_request_info* reqinfo, + netsnmp_request_info* requests) +{ + if (reqinfo->mode != MODE_GET) { + return SNMP_ERR_GENERR; + } + + if (reginfo->rootoid_len != OID_LENGTH(queriesOID) + 1) { + return SNMP_ERR_GENERR; + } + + const auto& it = s_statsMap.find(reginfo->rootoid[reginfo->rootoid_len - 2]); + if (it == s_statsMap.end()) { + return SNMP_ERR_GENERR; + } + + if (const auto& val = boost::get(&it->second)) { + std::string str(std::to_string(**val)); + snmp_set_var_typed_value(requests->requestvb, + ASN_OCTET_STR, + str.c_str(), + str.size()); + return SNMP_ERR_NOERROR; + } + + return SNMP_ERR_GENERR; +} + +static void registerFloatStat(const char* name, const oid statOID[], size_t statOIDLength, double* ptr) +{ + if (statOIDLength != OID_LENGTH(queriesOID)) { + errlog("Invalid OID for SNMP Float statistic %s", name); + return; + } + + if (s_statsMap.find(statOID[statOIDLength - 1]) != s_statsMap.end()) { + errlog("OID for SNMP Float statistic %s has already been registered", name); + return; + } + + s_statsMap[statOID[statOIDLength - 1]] = ptr; + netsnmp_register_scalar(netsnmp_create_handler_registration(name, + handleFloatStats, + statOID, + statOIDLength, + HANDLER_CAN_RONLY)); +} + +static int handleGauge64Stats(netsnmp_mib_handler* handler, + netsnmp_handler_registration* reginfo, + netsnmp_agent_request_info* reqinfo, + netsnmp_request_info* requests) +{ + if (reqinfo->mode != MODE_GET) { + return SNMP_ERR_GENERR; + } + + if (reginfo->rootoid_len != OID_LENGTH(queriesOID) + 1) { + return SNMP_ERR_GENERR; + } + + const auto& it = s_statsMap.find(reginfo->rootoid[reginfo->rootoid_len - 2]); + if (it == s_statsMap.end()) { + return SNMP_ERR_GENERR; + } + + std::string str; + uint64_t value = (*boost::get(&it->second))(str); + return DNSDistSNMPAgent::setCounter64Value(requests, value); +} + +static void registerGauge64Stat(const char* name, const oid statOID[], size_t statOIDLength, DNSDistStats::statfunction_t ptr) +{ + if (statOIDLength != OID_LENGTH(queriesOID)) { + errlog("Invalid OID for SNMP Gauge64 statistic %s", name); + return; + } + + if (s_statsMap.find(statOID[statOIDLength - 1]) != s_statsMap.end()) { + errlog("OID for SNMP Gauge64 statistic %s has already been registered", name); + return; + } + + s_statsMap[statOID[statOIDLength - 1]] = ptr; + netsnmp_register_scalar(netsnmp_create_handler_registration(name, + handleGauge64Stats, + statOID, + statOIDLength, + HANDLER_CAN_RONLY)); +} + +/* column number definitions for table backendStatTable */ +#define COLUMN_BACKENDID 1 +#define COLUMN_BACKENDNAME 2 +#define COLUMN_BACKENDLATENCY 3 +#define COLUMN_BACKENDWEIGHT 4 +#define COLUMN_BACKENDOUTSTANDING 5 +#define COLUMN_BACKENDQPSLIMIT 6 +#define COLUMN_BACKENDREUSED 7 +#define COLUMN_BACKENDSTATE 8 +#define COLUMN_BACKENDADDRESS 9 +#define COLUMN_BACKENDPOOLS 10 +#define COLUMN_BACKENDQPS 11 +#define COLUMN_BACKENDQUERIES 12 +#define COLUMN_BACKENDORDER 13 + +static const oid backendStatTableOID[] = { DNSDIST_STATS_TABLE_OID }; +static const oid backendNameOID[] = { DNSDIST_STATS_TABLE_OID, 1, 2 }; +static const oid backendStateOID[] = { DNSDIST_STATS_TABLE_OID, 1, 8}; +static const oid backendAddressOID[] = { DNSDIST_STATS_TABLE_OID, 1, 9}; + +static const oid socketFamilyOID[] = { DNSDIST_TRAP_OBJECTS_OID, 1, 0 }; +static const oid socketProtocolOID[] = { DNSDIST_TRAP_OBJECTS_OID, 2, 0 }; +static const oid fromAddressOID[] = { DNSDIST_TRAP_OBJECTS_OID, 3, 0 }; +static const oid toAddressOID[] = { DNSDIST_TRAP_OBJECTS_OID, 4, 0 }; +static const oid queryTypeOID[] = { DNSDIST_TRAP_OBJECTS_OID, 5, 0 }; +static const oid querySizeOID[] = { DNSDIST_TRAP_OBJECTS_OID, 6, 0 }; +static const oid queryIDOID[] = { DNSDIST_TRAP_OBJECTS_OID, 7, 0 }; +static const oid qNameOID[] = { DNSDIST_TRAP_OBJECTS_OID, 8, 0 }; +static const oid qClassOID[] = { DNSDIST_TRAP_OBJECTS_OID, 9, 0 }; +static const oid qTypeOID[] = { DNSDIST_TRAP_OBJECTS_OID, 10, 0 }; +static const oid trapReasonOID[] = { DNSDIST_TRAP_OBJECTS_OID, 11, 0 }; + +static const oid backendStatusChangeTrapOID[] = { DNSDIST_TRAPS_OID, 1 }; +static const oid actionTrapOID[] = { DNSDIST_TRAPS_OID, 2 }; +static const oid customTrapOID[] = { DNSDIST_TRAPS_OID, 3 }; + +static servers_t s_servers; +static size_t s_currentServerIdx = 0; + +static netsnmp_variable_list* backendStatTable_get_next_data_point(void** loop_context, + void** my_data_context, + netsnmp_variable_list* put_index_data, + netsnmp_iterator_info* mydata) +{ + if (s_currentServerIdx >= s_servers.size()) { + return NULL; + } + + *my_data_context = (void*) (s_servers[s_currentServerIdx]).get(); + snmp_set_var_typed_integer(put_index_data, ASN_UNSIGNED, s_currentServerIdx); + s_currentServerIdx++; + + return put_index_data; +} + +static netsnmp_variable_list* backendStatTable_get_first_data_point(void** loop_context, + void** data_context, + netsnmp_variable_list* put_index_data, + netsnmp_iterator_info* data) +{ + s_currentServerIdx = 0; + + /* get a copy of the shared_ptrs so they are not + destroyed while we process the request */ + const auto& dstates = g_dstates.getCopy(); + s_servers.clear(); + s_servers.reserve(dstates.size()); + for (const auto& server : dstates) { + s_servers.push_back(server); + } + + return backendStatTable_get_next_data_point(loop_context, + data_context, + put_index_data, + data); +} + +static int backendStatTable_handler(netsnmp_mib_handler* handler, + netsnmp_handler_registration* reginfo, + netsnmp_agent_request_info* reqinfo, + netsnmp_request_info* requests) +{ + netsnmp_request_info* request; + + switch (reqinfo->mode) { + case MODE_GET: + for (request = requests; request; request = request->next) { + netsnmp_table_request_info* table_info = netsnmp_extract_table_info(request); + const DownstreamState* server = (const DownstreamState*) netsnmp_extract_iterator_context(request); + + if (!server) { + continue; + } + + switch (table_info->colnum) { + case COLUMN_BACKENDNAME: + snmp_set_var_typed_value(request->requestvb, + ASN_OCTET_STR, + server->name.c_str(), + server->name.size()); + break; + case COLUMN_BACKENDLATENCY: + DNSDistSNMPAgent::setCounter64Value(request, + server->latencyUsec/1000.0); + break; + case COLUMN_BACKENDWEIGHT: + DNSDistSNMPAgent::setCounter64Value(request, + server->weight); + break; + case COLUMN_BACKENDOUTSTANDING: + DNSDistSNMPAgent::setCounter64Value(request, + server->outstanding); + break; + case COLUMN_BACKENDQPSLIMIT: + DNSDistSNMPAgent::setCounter64Value(request, + server->qps.getRate()); + break; + case COLUMN_BACKENDREUSED: + DNSDistSNMPAgent::setCounter64Value(request, server->reuseds); + break; + case COLUMN_BACKENDSTATE: + { + std::string state(server->getStatus()); + snmp_set_var_typed_value(request->requestvb, + ASN_OCTET_STR, + state.c_str(), + state.size()); + break; + } + case COLUMN_BACKENDADDRESS: + { + std::string addr(server->remote.toStringWithPort()); + snmp_set_var_typed_value(request->requestvb, + ASN_OCTET_STR, + addr.c_str(), + addr.size()); + break; + } + case COLUMN_BACKENDPOOLS: + { + std::string pools; + for(auto& p : server->pools) { + if(!pools.empty()) + pools+=" "; + pools+=p; + } + snmp_set_var_typed_value(request->requestvb, + ASN_OCTET_STR, + pools.c_str(), + pools.size()); + break; + } + case COLUMN_BACKENDQPS: + DNSDistSNMPAgent::setCounter64Value(request, server->queryLoad); + break; + case COLUMN_BACKENDQUERIES: + DNSDistSNMPAgent::setCounter64Value(request, server->queries); + break; + case COLUMN_BACKENDORDER: + DNSDistSNMPAgent::setCounter64Value(request, server->order); + break; + default: + netsnmp_set_request_error(reqinfo, + request, + SNMP_NOSUCHOBJECT); + break; + } + } + break; + } + return SNMP_ERR_NOERROR; +} +#endif /* HAVE_NET_SNMP */ + +bool DNSDistSNMPAgent::sendBackendStatusChangeTrap(const std::shared_ptr dss) +{ +#ifdef HAVE_NET_SNMP + const string backendAddress = dss->remote.toStringWithPort(); + const string backendStatus = dss->getStatus(); + netsnmp_variable_list* varList = nullptr; + + snmp_varlist_add_variable(&varList, + snmpTrapOID, + snmpTrapOIDLen, + ASN_OBJECT_ID, + backendStatusChangeTrapOID, + OID_LENGTH(backendStatusChangeTrapOID) * sizeof(oid)); + + + snmp_varlist_add_variable(&varList, + backendNameOID, + OID_LENGTH(backendNameOID), + ASN_OCTET_STR, + dss->name.c_str(), + dss->name.size()); + + snmp_varlist_add_variable(&varList, + backendAddressOID, + OID_LENGTH(backendAddressOID), + ASN_OCTET_STR, + backendAddress.c_str(), + backendAddress.size()); + + snmp_varlist_add_variable(&varList, + backendStateOID, + OID_LENGTH(backendStateOID), + ASN_OCTET_STR, + backendStatus.c_str(), + backendStatus.size()); + + return sendTrap(d_trapPipe[1], varList); +#endif /* HAVE_NET_SNMP */ + return true; +} + +bool DNSDistSNMPAgent::sendCustomTrap(const std::string& reason) +{ +#ifdef HAVE_NET_SNMP + netsnmp_variable_list* varList = nullptr; + + snmp_varlist_add_variable(&varList, + snmpTrapOID, + snmpTrapOIDLen, + ASN_OBJECT_ID, + customTrapOID, + OID_LENGTH(customTrapOID) * sizeof(oid)); + + snmp_varlist_add_variable(&varList, + trapReasonOID, + OID_LENGTH(trapReasonOID), + ASN_OCTET_STR, + reason.c_str(), + reason.size()); + + return sendTrap(d_trapPipe[1], varList); +#endif /* HAVE_NET_SNMP */ + return true; +} + +bool DNSDistSNMPAgent::sendDNSTrap(const DNSQuestion& dq, const std::string& reason) +{ +#ifdef HAVE_NET_SNMP + std::string local = dq.local->toString(); + std::string remote = dq.remote->toString(); + std::string qname = dq.qname->toStringNoDot(); + const uint32_t socketFamily = dq.remote->isIPv4() ? 1 : 2; + const uint32_t socketProtocol = dq.tcp ? 2 : 1; + const uint32_t queryType = dq.dh->qr ? 2 : 1; + const uint32_t querySize = (uint32_t) dq.len; + const uint32_t queryID = (uint32_t) ntohs(dq.dh->id); + const uint32_t qType = (uint32_t) dq.qtype; + const uint32_t qClass = (uint32_t) dq.qclass; + + netsnmp_variable_list* varList = nullptr; + + snmp_varlist_add_variable(&varList, + snmpTrapOID, + snmpTrapOIDLen, + ASN_OBJECT_ID, + actionTrapOID, + OID_LENGTH(actionTrapOID) * sizeof(oid)); + + snmp_varlist_add_variable(&varList, + socketFamilyOID, + OID_LENGTH(socketFamilyOID), + ASN_INTEGER, + (u_char *) &socketFamily, + sizeof(socketFamily)); + + snmp_varlist_add_variable(&varList, + socketProtocolOID, + OID_LENGTH(socketProtocolOID), + ASN_INTEGER, + (u_char *) &socketProtocol, + sizeof(socketProtocol)); + + snmp_varlist_add_variable(&varList, + fromAddressOID, + OID_LENGTH(fromAddressOID), + ASN_OCTET_STR, + remote.c_str(), + remote.size()); + + snmp_varlist_add_variable(&varList, + toAddressOID, + OID_LENGTH(toAddressOID), + ASN_OCTET_STR, + local.c_str(), + local.size()); + + snmp_varlist_add_variable(&varList, + queryTypeOID, + OID_LENGTH(queryTypeOID), + ASN_INTEGER, + (u_char *) &queryType, + sizeof(queryType)); + + snmp_varlist_add_variable(&varList, + querySizeOID, + OID_LENGTH(querySizeOID), + ASN_UNSIGNED, + (u_char *) &querySize, + sizeof(querySize)); + + snmp_varlist_add_variable(&varList, + queryIDOID, + OID_LENGTH(queryIDOID), + ASN_UNSIGNED, + (u_char *) &queryID, + sizeof(queryID)); + + snmp_varlist_add_variable(&varList, + qNameOID, + OID_LENGTH(qNameOID), + ASN_OCTET_STR, + qname.c_str(), + qname.size()); + + snmp_varlist_add_variable(&varList, + qClassOID, + OID_LENGTH(qClassOID), + ASN_UNSIGNED, + (u_char *) &qClass, + sizeof(qClass)); + + snmp_varlist_add_variable(&varList, + qTypeOID, + OID_LENGTH(qTypeOID), + ASN_UNSIGNED, + (u_char *) &qType, + sizeof(qType)); + + snmp_varlist_add_variable(&varList, + trapReasonOID, + OID_LENGTH(trapReasonOID), + ASN_OCTET_STR, + reason.c_str(), + reason.size()); + + return sendTrap(d_trapPipe[1], varList); +#endif /* HAVE_NET_SNMP */ + return true; +} + +DNSDistSNMPAgent::DNSDistSNMPAgent(const std::string& name, const std::string& masterSocket): SNMPAgent(name, masterSocket) +{ +#ifdef HAVE_NET_SNMP + + registerCounter64Stat("queries", queriesOID, OID_LENGTH(queriesOID), &g_stats.queries); + registerCounter64Stat("responses", responsesOID, OID_LENGTH(responsesOID), &g_stats.responses); + registerCounter64Stat("servfailResponses", servfailResponsesOID, OID_LENGTH(servfailResponsesOID), &g_stats.servfailResponses); + registerCounter64Stat("aclDrops", aclDropsOID, OID_LENGTH(aclDropsOID), &g_stats.aclDrops); + registerCounter64Stat("blockFilter", blockFilterOID, OID_LENGTH(blockFilterOID), &g_stats.blockFilter); + registerCounter64Stat("ruleDrop", ruleDropOID, OID_LENGTH(ruleDropOID), &g_stats.ruleDrop); + registerCounter64Stat("ruleNXDomain", ruleNXDomainOID, OID_LENGTH(ruleNXDomainOID), &g_stats.ruleNXDomain); + registerCounter64Stat("ruleRefused", ruleRefusedOID, OID_LENGTH(ruleRefusedOID), &g_stats.ruleRefused); + registerCounter64Stat("selfAnswered", selfAnsweredOID, OID_LENGTH(selfAnsweredOID), &g_stats.selfAnswered); + registerCounter64Stat("downstreamTimeouts", downstreamTimeoutsOID, OID_LENGTH(downstreamTimeoutsOID), &g_stats.downstreamTimeouts); + registerCounter64Stat("downstreamSendErrors", downstreamSendErrorsOID, OID_LENGTH(downstreamSendErrorsOID), &g_stats.downstreamSendErrors); + registerCounter64Stat("truncFail", truncFailOID, OID_LENGTH(truncFailOID), &g_stats.truncFail); + registerCounter64Stat("noPolicy", noPolicyOID, OID_LENGTH(noPolicyOID), &g_stats.noPolicy); + registerCounter64Stat("latency0_1", latency0_1OID, OID_LENGTH(latency0_1OID), &g_stats.latency0_1); + registerCounter64Stat("latency1_10", latency1_10OID, OID_LENGTH(latency1_10OID), &g_stats.latency1_10); + registerCounter64Stat("latency10_50", latency10_50OID, OID_LENGTH(latency10_50OID), &g_stats.latency10_50); + registerCounter64Stat("latency50_100", latency50_100OID, OID_LENGTH(latency50_100OID), &g_stats.latency50_100); + registerCounter64Stat("latency100_1000", latency100_1000OID, OID_LENGTH(latency100_1000OID), &g_stats.latency100_1000); + registerCounter64Stat("latencySlow", latencySlowOID, OID_LENGTH(latencySlowOID), &g_stats.latencySlow); + registerCounter64Stat("nonCompliantQueries", nonCompliantQueriesOID, OID_LENGTH(nonCompliantQueriesOID), &g_stats.nonCompliantQueries); + registerCounter64Stat("nonCompliantResponses", nonCompliantResponsesOID, OID_LENGTH(nonCompliantResponsesOID), &g_stats.nonCompliantResponses); + registerCounter64Stat("rdQueries", rdQueriesOID, OID_LENGTH(rdQueriesOID), &g_stats.rdQueries); + registerCounter64Stat("emptyQueries", emptyQueriesOID, OID_LENGTH(emptyQueriesOID), &g_stats.emptyQueries); + registerCounter64Stat("cacheHits", cacheHitsOID, OID_LENGTH(cacheHitsOID), &g_stats.cacheHits); + registerCounter64Stat("cacheMisses", cacheMissesOID, OID_LENGTH(cacheMissesOID), &g_stats.cacheMisses); + registerCounter64Stat("dynBlocked", dynBlockedOID, OID_LENGTH(dynBlockedOID), &g_stats.dynBlocked); + registerFloatStat("latencyAvg100", latencyAvg100OID, OID_LENGTH(latencyAvg100OID), &g_stats.latencyAvg100); + registerFloatStat("latencyAvg1000", latencyAvg1000OID, OID_LENGTH(latencyAvg1000OID), &g_stats.latencyAvg1000); + registerFloatStat("latencyAvg10000", latencyAvg10000OID, OID_LENGTH(latencyAvg10000OID), &g_stats.latencyAvg10000); + registerFloatStat("latencyAvg1000000", latencyAvg1000000OID, OID_LENGTH(latencyAvg1000000OID), &g_stats.latencyAvg1000000); + registerGauge64Stat("uptime", uptimeOID, OID_LENGTH(uptimeOID), &uptimeOfProcess); + registerGauge64Stat("realMemoryUsage", realMemoryUsageOID, OID_LENGTH(realMemoryUsageOID), &getRealMemoryUsage); + registerGauge64Stat("cpuUserMSec", cpuUserMSecOID, OID_LENGTH(cpuUserMSecOID), &getCPUTimeUser); + registerGauge64Stat("cpuSysMSec", cpuSysMSecOID, OID_LENGTH(cpuSysMSecOID), &getCPUTimeSystem); + registerGauge64Stat("fdUsage", fdUsageOID, OID_LENGTH(fdUsageOID), &getOpenFileDescriptors); + registerGauge64Stat("dynBlockedNMGSize", dynBlockedNMGSizeOID, OID_LENGTH(dynBlockedNMGSizeOID), [](const std::string&) { return g_dynblockNMG.getLocal()->size(); }); + + + netsnmp_table_registration_info* table_info = SNMP_MALLOC_TYPEDEF(netsnmp_table_registration_info); + netsnmp_table_helper_add_indexes(table_info, + ASN_GAUGE, /* index: backendId */ + 0); + table_info->min_column = COLUMN_BACKENDNAME; + table_info->max_column = COLUMN_BACKENDORDER; + netsnmp_iterator_info* iinfo = SNMP_MALLOC_TYPEDEF(netsnmp_iterator_info); + iinfo->get_first_data_point = backendStatTable_get_first_data_point; + iinfo->get_next_data_point = backendStatTable_get_next_data_point; + iinfo->table_reginfo = table_info; + + netsnmp_register_table_iterator(netsnmp_create_handler_registration("backendStatTable", + backendStatTable_handler, + backendStatTableOID, + OID_LENGTH(backendStatTableOID), + HANDLER_CAN_RONLY), + iinfo); + +#endif /* HAVE_NET_SNMP */ +} diff --git a/pdns/dnsdist-snmp.hh b/pdns/dnsdist-snmp.hh new file mode 100644 index 0000000000..bced61aab0 --- /dev/null +++ b/pdns/dnsdist-snmp.hh @@ -0,0 +1,21 @@ +#ifndef DNSDIST_SNMP_HH +#define DNSDIST_SNMP_HH + +#pragma once + +#include "snmp-agent.hh" + +class DNSDistSNMPAgent; + +#include "dnsdist.hh" + +class DNSDistSNMPAgent: public SNMPAgent +{ +public: + DNSDistSNMPAgent(const std::string& name, const std::string& masterSocket); + bool sendBackendStatusChangeTrap(const std::shared_ptr); + bool sendCustomTrap(const std::string& reason); + bool sendDNSTrap(const DNSQuestion&, const std::string& reason=""); +}; + +#endif /* DNSDIST_SNMP_HH */ diff --git a/pdns/dnsdist.cc b/pdns/dnsdist.cc index eb5da37495..dd8c8b65e2 100644 --- a/pdns/dnsdist.cc +++ b/pdns/dnsdist.cc @@ -89,6 +89,10 @@ std::vector > g_dynBPFFilters; vector g_frontends; GlobalStateHolder g_pools; +bool g_snmpEnabled{false}; +bool g_snmpTrapsEnabled{false}; +DNSDistSNMPAgent* g_snmpAgent{nullptr}; + /* UDP: the grand design. Per socket we listen on for incoming queries there is one thread. Then we have a bunch of connected sockets for talking to downstream servers. We send directly to those sockets. @@ -1484,6 +1488,9 @@ void* healthChecksThread() dss->upStatus = newState; dss->currentCheckFailures = 0; + if (g_snmpAgent && g_snmpTrapsEnabled) { + g_snmpAgent->sendBackendStatusChangeTrap(dss); + } } } @@ -1881,7 +1888,6 @@ try if(g_locals.empty()) g_locals.push_back(std::make_tuple(ComboAddress("127.0.0.1", 53), true, false, 0)); - g_configurationDone = true; @@ -2085,6 +2091,10 @@ try /* this need to be done _after_ dropping privileges */ g_delay = new DelayPipe(); + if (g_snmpAgent) { + g_snmpAgent->run(); + } + g_tcpclientthreads = std::make_shared(g_maxTCPClientThreads, g_useTCPSinglePipe); for(auto& t : todo) diff --git a/pdns/dnsdist.hh b/pdns/dnsdist.hh index db256d85cf..31904c0036 100644 --- a/pdns/dnsdist.hh +++ b/pdns/dnsdist.hh @@ -507,6 +507,17 @@ struct DownstreamState } return name + " (" + remote.toStringWithPort()+ ")"; } + string getStatus() const + { + string status; + if(availability == DownstreamState::Availability::Up) + status = "UP"; + else if(availability == DownstreamState::Availability::Down) + status = "DOWN"; + else + status = (upStatus ? "up" : "down"); + return status; + } void reconnect(); }; using servers_t =vector>; @@ -730,3 +741,9 @@ extern std::vector> g_dnsCrypt int handleDnsCryptQuery(DnsCryptContext* ctx, char* packet, uint16_t len, std::shared_ptr& query, uint16_t* decryptedQueryLen, bool tcp, std::vector& response); bool encryptResponse(char* response, uint16_t* responseLen, size_t responseSize, bool tcp, std::shared_ptr dnsCryptQuery); #endif + +#include "dnsdist-snmp.hh" + +extern bool g_snmpEnabled; +extern bool g_snmpTrapsEnabled; +extern DNSDistSNMPAgent* g_snmpAgent; diff --git a/pdns/dnsdistdist/DNSDIST-MIB.txt b/pdns/dnsdistdist/DNSDIST-MIB.txt new file mode 120000 index 0000000000..e406bb9376 --- /dev/null +++ b/pdns/dnsdistdist/DNSDIST-MIB.txt @@ -0,0 +1 @@ +../../docs/MIBS/DNSDIST-MIB.txt \ No newline at end of file diff --git a/pdns/dnsdistdist/Makefile.am b/pdns/dnsdistdist/Makefile.am index 2aa1c3979c..2189be1927 100644 --- a/pdns/dnsdistdist/Makefile.am +++ b/pdns/dnsdistdist/Makefile.am @@ -1,4 +1,4 @@ -AM_CPPFLAGS += $(SYSTEMD_CFLAGS) $(LUA_CFLAGS) $(LIBEDIT_CFLAGS) $(YAHTTP_CFLAGS) $(SANITIZER_FLAGS) -DSYSCONFDIR=\"${sysconfdir}\" +AM_CPPFLAGS += $(SYSTEMD_CFLAGS) $(LUA_CFLAGS) $(LIBEDIT_CFLAGS) $(YAHTTP_CFLAGS) $(SANITIZER_FLAGS) $(NET_SNMP_CFLAGS) -DSYSCONFDIR=\"${sysconfdir}\" ACLOCAL_AMFLAGS = -I m4 @@ -45,7 +45,8 @@ EXTRA_DIST=dnslabeltext.rl \ lua_hpp.mk \ bpf-filter.main.ebpf \ bpf-filter.qname.ebpf \ - bpf-filter.ebpf.src + bpf-filter.ebpf.src \ + DNSDIST-MIB.txt bin_PROGRAMS = dnsdist @@ -77,6 +78,7 @@ dnsdist_SOURCES = \ dnsdist-lua2.cc \ dnsdist-protobuf.cc dnsdist-protobuf.hh \ dnsdist-rings.cc \ + dnsdist-snmp.cc dnsdist-snmp.hh \ dnsdist-tcp.cc \ dnsdist-web.cc \ dnslabeltext.cc \ @@ -99,6 +101,7 @@ dnsdist_SOURCES = \ qtype.cc qtype.hh \ remote_logger.cc remote_logger.hh \ sholder.hh \ + snmp-agent.cc snmp-agent.hh \ sodcrypto.cc sodcrypto.hh \ sstuff.hh \ statnode.cc statnode.hh \ @@ -120,7 +123,8 @@ dnsdist_LDADD = \ $(YAHTTP_LIBS) \ $(LIBSODIUM_LIBS) \ $(SANITIZER_FLAGS) \ - $(SYSTEMD_LIBS) + $(SYSTEMD_LIBS) \ + $(NET_SNMP_LIBS) if HAVE_RE2 dnsdist_LDADD += $(RE2_LIBS) diff --git a/pdns/dnsdistdist/configure.ac b/pdns/dnsdistdist/configure.ac index 53d9a9aa30..717cb17db6 100644 --- a/pdns/dnsdistdist/configure.ac +++ b/pdns/dnsdistdist/configure.ac @@ -35,6 +35,7 @@ PDNS_ENABLE_UNIT_TESTS PDNS_CHECK_RE2 DNSDIST_ENABLE_DNSCRYPT PDNS_WITH_EBPF +PDNS_WITH_NET_SNMP AX_AVAILABLE_SYSTEMD AM_CONDITIONAL([HAVE_SYSTEMD], [ test x"$systemd" = "xy" ]) @@ -138,4 +139,8 @@ AS_IF([test "x$RE2_LIBS" != "x"], [AC_MSG_NOTICE([re2: yes])], [AC_MSG_NOTICE([re2: no])] ) +AS_IF([test "x$NET_SNMP_LIBS" != "x"], + [AC_MSG_NOTICE([SNMP: yes])], + [AC_MSG_NOTICE([SNMP: no])] +) AC_MSG_NOTICE([]) diff --git a/pdns/dnsdistdist/dnsdist-snmp.cc b/pdns/dnsdistdist/dnsdist-snmp.cc new file mode 120000 index 0000000000..49f9feda64 --- /dev/null +++ b/pdns/dnsdistdist/dnsdist-snmp.cc @@ -0,0 +1 @@ +../dnsdist-snmp.cc \ No newline at end of file diff --git a/pdns/dnsdistdist/dnsdist-snmp.hh b/pdns/dnsdistdist/dnsdist-snmp.hh new file mode 120000 index 0000000000..ffa4710635 --- /dev/null +++ b/pdns/dnsdistdist/dnsdist-snmp.hh @@ -0,0 +1 @@ +../dnsdist-snmp.hh \ No newline at end of file diff --git a/pdns/dnsdistdist/m4/pdns_with_net_snmp.m4 b/pdns/dnsdistdist/m4/pdns_with_net_snmp.m4 new file mode 120000 index 0000000000..676095ba0e --- /dev/null +++ b/pdns/dnsdistdist/m4/pdns_with_net_snmp.m4 @@ -0,0 +1 @@ +../../../m4/pdns_with_net_snmp.m4 \ No newline at end of file diff --git a/pdns/dnsdistdist/snmp-agent.cc b/pdns/dnsdistdist/snmp-agent.cc new file mode 120000 index 0000000000..d786b47937 --- /dev/null +++ b/pdns/dnsdistdist/snmp-agent.cc @@ -0,0 +1 @@ +../snmp-agent.cc \ No newline at end of file diff --git a/pdns/dnsdistdist/snmp-agent.hh b/pdns/dnsdistdist/snmp-agent.hh new file mode 120000 index 0000000000..b5ddc99882 --- /dev/null +++ b/pdns/dnsdistdist/snmp-agent.hh @@ -0,0 +1 @@ +../snmp-agent.hh \ No newline at end of file diff --git a/pdns/dnsrulactions.hh b/pdns/dnsrulactions.hh index a964a3467f..eec9cb24b5 100644 --- a/pdns/dnsrulactions.hh +++ b/pdns/dnsrulactions.hh @@ -1102,6 +1102,28 @@ private: boost::optional > d_alterFunc; }; +class SNMPTrapAction : public DNSAction +{ +public: + SNMPTrapAction(const std::string& reason): d_reason(reason) + { + } + DNSAction::Action operator()(DNSQuestion* dq, string* ruleresult) const override + { + if (g_snmpAgent && g_snmpTrapsEnabled) { + g_snmpAgent->sendDNSTrap(*dq, d_reason); + } + + return Action::None; + } + string toString() const override + { + return "send SNMP trap"; + } +private: + std::string d_reason; +}; + class RemoteLogResponseAction : public DNSResponseAction, public boost::noncopyable { public: @@ -1177,3 +1199,25 @@ public: private: int d_msec; }; + +class SNMPTrapResponseAction : public DNSResponseAction +{ +public: + SNMPTrapResponseAction(const std::string& reason): d_reason(reason) + { + } + DNSResponseAction::Action operator()(DNSResponse* dr, string* ruleresult) const override + { + if (g_snmpAgent && g_snmpTrapsEnabled) { + g_snmpAgent->sendDNSTrap(*dr, d_reason); + } + + return Action::None; + } + string toString() const override + { + return "send SNMP trap"; + } +private: + std::string d_reason; +}; diff --git a/pdns/snmp-agent.cc b/pdns/snmp-agent.cc new file mode 100644 index 0000000000..49c1747f4f --- /dev/null +++ b/pdns/snmp-agent.cc @@ -0,0 +1,121 @@ +#include "snmp-agent.hh" +#include "misc.hh" + +#ifdef HAVE_NET_SNMP + +const oid SNMPAgent::snmpTrapOID[] = { 1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0 }; +const size_t SNMPAgent::snmpTrapOIDLen = OID_LENGTH(SNMPAgent::snmpTrapOID); + +int SNMPAgent::setCounter64Value(netsnmp_request_info* request, + uint64_t value) +{ + struct counter64 val64; + val64.high = value >> 32; + val64.low = value & 0xffffffff; + snmp_set_var_typed_value(request->requestvb, + ASN_COUNTER64, + &val64, + sizeof(val64)); + return SNMP_ERR_NOERROR; +} + +bool SNMPAgent::sendTrap(int fd, + netsnmp_variable_list* varList) +{ + ssize_t written = write(fd, &varList, sizeof(varList)); + + if (written != sizeof(varList)) { + snmp_free_varbind(varList); + return false; + } + return true; +} + +void SNMPAgent::handleTraps() +{ + netsnmp_variable_list* varList = nullptr; + ssize_t got = 0; + + do { + got = read(d_trapPipe[0], &varList, sizeof(varList)); + + if (got == sizeof(varList)) { + send_v2trap(varList); + snmp_free_varbind(varList); + } + } + while (got > 0); +} +#endif /* HAVE_NET_SNMP */ + +void SNMPAgent::worker() +{ +#ifdef HAVE_NET_SNMP + int numfds = 0; + int block = 1; + fd_set fdset; + struct timeval timeout = { 0, 0 }; + + while(true) { + numfds = FD_SETSIZE; + + FD_ZERO(&fdset); + FD_SET(d_trapPipe[0], &fdset); + snmp_select_info(&numfds, &fdset, &timeout, &block); + + int res = select(FD_SETSIZE, &fdset, NULL, NULL, NULL); + + if (res == 2) { + FD_CLR(d_trapPipe[0], &fdset); + snmp_read(&fdset); + handleTraps(); + } + else if (res == 1) + { + if (FD_ISSET(d_trapPipe[0], &fdset)) { + handleTraps(); + } else { + snmp_read(&fdset); + } + } + else if (res == 0) { + snmp_timeout(); + } + } +#endif /* HAVE_NET_SNMP */ +} + +SNMPAgent::SNMPAgent(const std::string& name, const std::string& masterSocket) +{ +#ifdef HAVE_NET_SNMP + netsnmp_enable_subagent(); + snmp_disable_log(); + if (!masterSocket.empty()) { + netsnmp_ds_set_string(NETSNMP_DS_APPLICATION_ID, + NETSNMP_DS_AGENT_X_SOCKET, + masterSocket.c_str()); + } + /* no need to load any MIBS, + and it causes import errors if some modules are not present */ + setenv("MIBS", "", 1); + + init_agent(name.c_str()); + init_snmp(name.c_str()); + + if (pipe(d_trapPipe) < 0) + unixDie("Creating pipe"); + + if (!setNonBlocking(d_trapPipe[0])) { + close(d_trapPipe[0]); + close(d_trapPipe[1]); + unixDie("Setting pipe non-blocking"); + } + + if (!setNonBlocking(d_trapPipe[1])) { + close(d_trapPipe[0]); + close(d_trapPipe[1]); + unixDie("Setting pipe non-blocking"); + } + +#endif /* HAVE_NET_SNMP */ +} diff --git a/pdns/snmp-agent.hh b/pdns/snmp-agent.hh new file mode 100644 index 0000000000..cc22ed98b8 --- /dev/null +++ b/pdns/snmp-agent.hh @@ -0,0 +1,59 @@ +#ifndef SNMP_AGENT_HH +#define SNMP_AGENT_HH + +#include "config.h" + +#include +#include +#include + +#ifdef HAVE_NET_SNMP +#include +#include +#include +#undef INET6 /* SRSLY? */ +#endif /* HAVE_NET_SNMP */ + +class SNMPAgent +{ +public: + SNMPAgent(const std::string& name, const std::string& masterSocket); + virtual ~SNMPAgent() + { +#ifdef HAVE_NET_SNMP + close(d_trapPipe[0]); + close(d_trapPipe[1]); +#endif /* HAVE_NET_SNMP */ + } + + void run() + { +#ifdef HAVE_NET_SNMP + d_thread = std::move(std::thread(&SNMPAgent::worker, this)); +#endif /* HAVE_NET_SNMP */ + } + +#ifdef HAVE_NET_SNMP + static int setCounter64Value(netsnmp_request_info* request, + uint64_t value); +#endif /* HAVE_NET_SNMP */ +protected: +#ifdef HAVE_NET_SNMP + /* OID for snmpTrapOID.0 */ + static const oid snmpTrapOID[]; + static const size_t snmpTrapOIDLen; + + static bool sendTrap(int fd, + netsnmp_variable_list* varList); + + void handleTraps(); + + int d_trapPipe[2] = { -1, -1}; +#endif /* HAVE_NET_SNMP */ +private: + void worker(); + + std::thread d_thread; +}; + +#endif /* SNMP_AGENT_HH */ diff --git a/regression-tests.dnsdist/requirements.txt b/regression-tests.dnsdist/requirements.txt index 1a878ab9ad..355698af2c 100644 --- a/regression-tests.dnsdist/requirements.txt +++ b/regression-tests.dnsdist/requirements.txt @@ -3,3 +3,4 @@ nose>=1.3.7 libnacl>=1.4.3 requests>=2.1.0 protobuf>=2.5,<3.0 +pysnmp>=4.3.2 diff --git a/regression-tests.dnsdist/snmpd.conf b/regression-tests.dnsdist/snmpd.conf new file mode 100644 index 0000000000..de490f6bc3 --- /dev/null +++ b/regression-tests.dnsdist/snmpd.conf @@ -0,0 +1,13 @@ + +# act as an Agent X master so that dnsdist can export SNMP statistics +master agentx + +# allow dnsdist to connect to the Agent X master socket +agentxperms 0700 0755 dnsdist + +# SNMPv2c community +rocommunity secretcommunity + +# SNMPv3 user +createUser secretuser SHA "mysecretauthkey" AES "mysecretenckey" +rouser secretuser diff --git a/regression-tests.dnsdist/test_SNMP.py b/regression-tests.dnsdist/test_SNMP.py new file mode 100644 index 0000000000..014b675a67 --- /dev/null +++ b/regression-tests.dnsdist/test_SNMP.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python +import dns +import time + +from pysnmp.hlapi import * +from dnsdisttests import DNSDistTest + +class TestSNMP(DNSDistTest): + + _snmpTimeout = 2.0 + _snmpServer = '127.0.0.1' + _snmpPort = 161 + _snmpV2Community = 'secretcommunity' + _snmpV3User = 'secretuser' + _snmpV3AuthKey = 'mysecretauthkey' + _snmpV3EncKey = 'mysecretenckey' + _snmpOID = '1.3.6.1.4.1.43315.3' + _queriesSent = 0 + _config_template = """ + newServer{address="127.0.0.1:%s", name="servername"} + snmpAgent(true) + """ + + def _checkStatsValues(self, results, queriesCountersValue): + for i in range(1, 20) + range(24, 35) + [ 35 ] : + oid = self._snmpOID + '.1.' + str(i) + '.0' + self.assertTrue(oid in results) + self.assertTrue(isinstance(results[oid], Counter64)) + + for i in range(20, 23): + oid = self._snmpOID + '.1.' + str(i) + '.0' + self.assertTrue(isinstance(results[oid], OctetString)) + + # check uptime > 0 + self.assertGreater(results['1.3.6.1.4.1.43315.3.1.24.0'], 0) + # check memory usage > 0 + self.assertGreater(results['1.3.6.1.4.1.43315.3.1.25.0'], 0) + + # check that the queries, responses and rdQueries counters are now at queriesCountersValue + for i in [1, 2, 28]: + oid = self._snmpOID + '.1.' + str(i) + '.0' + self.assertEquals(results[oid], queriesCountersValue) + + # the others counters (except for latency ones) should still be at 0 + for i in range(3, 14) + [26, 27, 29, 30, 31, 35, 36]: + oid = self._snmpOID + '.1.' + str(i) + '.0' + self.assertEquals(results[oid], 0) + + # check the backend stats + print(results) + + ## types + for i in [3, 4, 5, 6, 7, 11, 12, 13]: + oid = self._snmpOID + '.2.1.' + str(i) + '.0' + self.assertTrue(isinstance(results[oid], Counter64)) + for i in [2, 8, 9, 10]: + oid = self._snmpOID + '.2.1.' + str(i) + '.0' + self.assertTrue(isinstance(results[oid], OctetString)) + + ## name + self.assertEquals(results['1.3.6.1.4.1.43315.3.2.1.2.0'], "servername") + ## weight + self.assertEquals(results['1.3.6.1.4.1.43315.3.2.1.4.0'], 1) + ## outstanding + self.assertEquals(results['1.3.6.1.4.1.43315.3.2.1.5.0'], 0) + ## qpslimit + self.assertEquals(results['1.3.6.1.4.1.43315.3.2.1.6.0'], 0) + ## reused + self.assertEquals(results['1.3.6.1.4.1.43315.3.2.1.7.0'], 0) + ## state + self.assertEquals(results['1.3.6.1.4.1.43315.3.2.1.8.0'], "up") + ## address + self.assertEquals(results['1.3.6.1.4.1.43315.3.2.1.9.0'], ("127.0.0.1:%s" % (self._testServerPort))) + ## pools + self.assertEquals(results['1.3.6.1.4.1.43315.3.2.1.10.0'], "") + ## queries + self.assertEquals(results['1.3.6.1.4.1.43315.3.2.1.12.0'], queriesCountersValue) + ## order + self.assertEquals(results['1.3.6.1.4.1.43315.3.2.1.13.0'], 1) + + def _getSNMPStats(self, auth): + results = {} + for (errorIndication, errorStatus, errorIndex, varBinds) in nextCmd(SnmpEngine(), + auth, + UdpTransportTarget((self._snmpServer, self._snmpPort), timeout=self._snmpTimeout), + ContextData(), + ObjectType(ObjectIdentity(self._snmpOID)), + lookupMib=False): + self.assertFalse(errorIndication) + self.assertFalse(errorStatus) + self.assertTrue(varBinds) + for key, value in varBinds: + keystr = key.prettyPrint() + if not keystr.startswith(self._snmpOID): + continue + results[keystr] = value + + return results + + def _checkStats(self, auth, name): + # wait 1s so that the uptime is > 0 + time.sleep(1) + + results = self._getSNMPStats(auth) + self._checkStatsValues(results, self.__class__._queriesSent) + + query = dns.message.make_query(name, 'A', 'IN', use_edns=False) + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.A, + '127.0.0.1') + response.answer.append(rrset) + + # send a query + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + self.__class__._queriesSent = self.__class__._queriesSent + 1 + + results = self._getSNMPStats(auth) + self._checkStatsValues(results, self.__class__._queriesSent) + + def testSNMPv2Stats(self): + """ + SNMP: Retrieve statistics via SNMPv2c + """ + + auth = CommunityData(self._snmpV2Community, mpModel=1) + name = 'simplea.snmpv2c.tests.powerdns.com.' + self._checkStats(auth, name) + + def testSNMPv3Stats(self): + """ + SNMP: Retrieve statistics via SNMPv3 + """ + + auth = UsmUserData(self._snmpV3User, + authKey=self._snmpV3AuthKey, + privKey=self._snmpV3EncKey, + authProtocol=usmHMACSHAAuthProtocol, + privProtocol=usmAesCfb128Protocol) + name = 'simplea.snmpv2.tests.powerdns.com.' + self._checkStats(auth, name) -- 2.47.2