]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Add SNMP support 4989/head
authorRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 5 Dec 2016 14:59:58 +0000 (15:59 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 6 Feb 2017 13:39:50 +0000 (14:39 +0100)
25 files changed:
build-scripts/travis.sh
docs/MIBS/DNSDIST-MIB.txt [new file with mode: 0644]
m4/pdns_with_net_snmp.m4 [new file with mode: 0644]
pdns/README-dnsdist.md
pdns/dnsdist-console.cc
pdns/dnsdist-lua.cc
pdns/dnsdist-lua2.cc
pdns/dnsdist-snmp.cc [new file with mode: 0644]
pdns/dnsdist-snmp.hh [new file with mode: 0644]
pdns/dnsdist.cc
pdns/dnsdist.hh
pdns/dnsdistdist/DNSDIST-MIB.txt [new symlink]
pdns/dnsdistdist/Makefile.am
pdns/dnsdistdist/configure.ac
pdns/dnsdistdist/dnsdist-snmp.cc [new symlink]
pdns/dnsdistdist/dnsdist-snmp.hh [new symlink]
pdns/dnsdistdist/m4/pdns_with_net_snmp.m4 [new symlink]
pdns/dnsdistdist/snmp-agent.cc [new symlink]
pdns/dnsdistdist/snmp-agent.hh [new symlink]
pdns/dnsrulactions.hh
pdns/snmp-agent.cc [new file with mode: 0644]
pdns/snmp-agent.hh [new file with mode: 0644]
regression-tests.dnsdist/requirements.txt
regression-tests.dnsdist/snmpd.conf [new file with mode: 0644]
regression-tests.dnsdist/test_SNMP.py [new file with mode: 0644]

index c4694d01f93c88839371b836cdeb51c889d15203..630da302b5522c2d28613596a70b68ae5c37b6ce 100755 (executable)
@@ -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 (file)
index 0000000..7eba714
--- /dev/null
@@ -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 (file)
index 0000000..4134fc4
--- /dev/null
@@ -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.])])
+])
index c41f94542d114aeedaf0bf14352c4be90692c4f1..5e69288d534dc4e0df89c8278c55b973c216fcec 100644 (file)
@@ -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
 ---------
index f4dec4ad51e5a37130af132e75fe7a81638fc2c5..c54d2f6c19c95f172a602ffa0b3cec0d14b7b569 100644 (file)
@@ -352,6 +352,7 @@ const std::vector<ConsoleKeyword> 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<ConsoleKeyword> 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" },
index b6687597d1b2c3c2bd0ac6f1fc3038074bedf490..0b3867665aaf51f8d8a18a651b934dd5b4fa265c 100644 (file)
@@ -679,14 +679,7 @@ vector<std::function<void(void)>> 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<std::function<void(void)>> setupLua(bool client, const std::string& confi
   g_lua.registerFunction<bool(DNSQuestion::*)()>("getDO", [](const DNSQuestion& dq) {
       return getEDNSZ((const char*)dq.dh, dq.len) & EDNS_HEADER_FLAG_DO;
     });
+  g_lua.registerFunction<void(DNSQuestion::*)(std::string)>("sendTrap", [](const DNSQuestion& dq, boost::optional<std::string> 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<const ComboAddress (DNSResponse::*)>("localaddr", [](const DNSResponse& dq) -> const ComboAddress { return *dq.local; }, [](DNSResponse& dq, const ComboAddress newLocal) { (void) newLocal; });
@@ -1559,6 +1559,13 @@ vector<std::function<void(void)>> setupLua(bool client, const std::string& confi
   g_lua.registerFunction<void(DNSResponse::*)(std::function<uint32_t(uint8_t section, uint16_t qclass, uint16_t qtype, uint32_t ttl)> editFunc)>("editTTLs", [](const DNSResponse& dr, std::function<uint32_t(uint8_t section, uint16_t qclass, uint16_t qtype, uint32_t ttl)> editFunc) {
         editDNSPacketTTL((char*) dr.dh, dr.len, editFunc);
       });
+  g_lua.registerFunction<void(DNSResponse::*)(std::string)>("sendTrap", [](const DNSResponse& dr, boost::optional<std::string> 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) {
index 1455b5a73454296ab4131b9e231657e0f09ed569..25708a01585236e37dc5eb8a37764ddc5c12a108 100644 (file)
@@ -1245,4 +1245,50 @@ void moreLua(bool client)
         g_useTCPSinglePipe = flag;
       });
 
+    g_lua.writeFunction("snmpAgent", [](bool enableTraps, boost::optional<std::string> 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<std::string> reason) {
+#ifdef HAVE_NET_SNMP
+        return std::shared_ptr<DNSAction>(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<std::string> reason) {
+#ifdef HAVE_NET_SNMP
+        return std::shared_ptr<DNSResponseAction>(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 (file)
index 0000000..e3528eb
--- /dev/null
@@ -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<oid, DNSDistStats::entry_t> 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<DNSDistStats::stat_t*>(&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<uint64_t>* 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<double*>(&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<DNSDistStats::statfunction_t>(&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<DownstreamState> 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 (file)
index 0000000..bced61a
--- /dev/null
@@ -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<DownstreamState>);
+  bool sendCustomTrap(const std::string& reason);
+  bool sendDNSTrap(const DNSQuestion&, const std::string& reason="");
+};
+
+#endif /* DNSDIST_SNMP_HH */
index eb5da3749515a65b21ede5b8b0241c8ca5588462..dd8c8b65e2851fe2a104f5fd9cb34f92ab39714e 100644 (file)
@@ -89,6 +89,10 @@ std::vector<std::shared_ptr<DynBPFFilter> > g_dynBPFFilters;
 vector<ClientState *> g_frontends;
 GlobalStateHolder<pools_t> 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<DelayedPacket>();
 
+  if (g_snmpAgent) {
+    g_snmpAgent->run();
+  }
+
   g_tcpclientthreads = std::make_shared<TCPClientCollection>(g_maxTCPClientThreads, g_useTCPSinglePipe);
 
   for(auto& t : todo)
index db256d85cf2db32f2854043977856db53c25fbfc..31904c0036249cd86c48e5fd852d71625c8d4056 100644 (file)
@@ -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<std::shared_ptr<DownstreamState>>;
@@ -730,3 +741,9 @@ extern std::vector<std::tuple<ComboAddress,DnsCryptContext,bool,int>> g_dnsCrypt
 int handleDnsCryptQuery(DnsCryptContext* ctx, char* packet, uint16_t len, std::shared_ptr<DnsCryptQuery>& query, uint16_t* decryptedQueryLen, bool tcp, std::vector<uint8_t>& response);
 bool encryptResponse(char* response, uint16_t* responseLen, size_t responseSize, bool tcp, std::shared_ptr<DnsCryptQuery> 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 (symlink)
index 0000000..e406bb9
--- /dev/null
@@ -0,0 +1 @@
+../../docs/MIBS/DNSDIST-MIB.txt
\ No newline at end of file
index 2aa1c3979c6fea3aa91481586c01c007411bacf9..2189be192786fa303b44f2f2414e6a960349aa9d 100644 (file)
@@ -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)
index 53d9a9aa300ca36ffcbd943597a26fee87cd7b6f..717cb17db676b15560fed2a1c8b02f957226f1a5 100644 (file)
@@ -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 (symlink)
index 0000000..49f9fed
--- /dev/null
@@ -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 (symlink)
index 0000000..ffa4710
--- /dev/null
@@ -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 (symlink)
index 0000000..676095b
--- /dev/null
@@ -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 (symlink)
index 0000000..d786b47
--- /dev/null
@@ -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 (symlink)
index 0000000..b5ddc99
--- /dev/null
@@ -0,0 +1 @@
+../snmp-agent.hh
\ No newline at end of file
index a964a3467ff9add331a774f18d4865f58db088d2..eec9cb24b5956a6706f19162ee4c24310a32be6e 100644 (file)
@@ -1102,6 +1102,28 @@ private:
   boost::optional<std::function<void(const DNSQuestion&, DNSDistProtoBufMessage*)> > 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 (file)
index 0000000..49c1747
--- /dev/null
@@ -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 (file)
index 0000000..cc22ed9
--- /dev/null
@@ -0,0 +1,59 @@
+#ifndef SNMP_AGENT_HH
+#define SNMP_AGENT_HH
+
+#include "config.h"
+
+#include <string>
+#include <thread>
+#include <unistd.h>
+
+#ifdef HAVE_NET_SNMP
+#include <net-snmp/net-snmp-config.h>
+#include <net-snmp/net-snmp-includes.h>
+#include <net-snmp/agent/net-snmp-agent-includes.h>
+#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 */
index 1a878ab9ad017a0e6979f240e14e653edf2113e1..355698af2cb9b464fd3bd8bbb37a53bcbbc7c81f 100644 (file)
@@ -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 (file)
index 0000000..de490f6
--- /dev/null
@@ -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 (file)
index 0000000..014b675
--- /dev/null
@@ -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)