summaryrefslogtreecommitdiffstats
path: root/src/output/dnssim.lua
diff options
context:
space:
mode:
Diffstat (limited to 'src/output/dnssim.lua')
-rw-r--r--src/output/dnssim.lua433
1 files changed, 433 insertions, 0 deletions
diff --git a/src/output/dnssim.lua b/src/output/dnssim.lua
new file mode 100644
index 0000000..25193c4
--- /dev/null
+++ b/src/output/dnssim.lua
@@ -0,0 +1,433 @@
+-- Copyright (c) 2018-2021, CZ.NIC, z.s.p.o.
+-- All rights reserved.
+--
+-- This file is part of dnsjit.
+--
+-- dnsjit is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- dnsjit is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with dnsjit. If not, see <http://www.gnu.org/licenses/>.
+
+-- dnsjit.output.dnssim
+-- Simulate independent DNS clients over various transports
+-- output = require("dnsjit.output.dnssim").new()
+-- .SS Usage
+-- output:udp()
+-- output:target("::1", 53)
+-- recv, rctx = output:receive()
+-- -- pass in objects using recv(rctx, obj)
+-- -- repeatedly call output:run_nowait() until it returns 0
+-- .SS DNS-over-TLS example configuration
+-- output:tls("NORMAL:-VERS-ALL:+VERS-TLS1.3") -- enforce TLS 1.3
+-- .SS DNS-over-HTTPS/2 example configuration
+-- output:https2({ method = "POST", uri_path = "/doh" })
+--
+-- Output module for simulating traffic from huge number of independent,
+-- individual DNS clients.
+-- Uses libuv for asynchronous communication.
+-- There may only be a single DnsSim in a thread.
+-- Use
+-- .I dnsjit.core.thread
+-- to have multiple DnsSim instances.
+-- .P
+-- With proper use of this component, it is possible to simulate hundreds of
+-- thousands of clients when using a high-performance server.
+-- This also applies for state-full transports.
+-- The complete set-up is quite complex and requires other components.
+-- See DNS Shotgun
+-- .RI ( https://gitlab.nic.cz/knot/shotgun )
+-- for dnsjit scripts ready for use for high-performance
+-- benchmarking.
+module(...,package.seeall)
+
+require("dnsjit.output.dnssim_h")
+local bit = require("bit")
+local object = require("dnsjit.core.objects")
+local ffi = require("ffi")
+local C = ffi.C
+
+local DnsSim = {}
+
+local _DNSSIM_VERSION = 20210129
+local _DNSSIM_JSON_VERSION = 20200527
+
+-- Create a new DnsSim output for up to max_clients.
+function DnsSim.new(max_clients)
+ local self = {
+ obj = C.output_dnssim_new(max_clients),
+ max_clients = max_clients,
+ }
+ ffi.gc(self.obj, C.output_dnssim_free)
+ return setmetatable(self, { __index = DnsSim })
+end
+
+local function _check_version(version, req_version)
+ if req_version == nil then
+ return version
+ end
+ local min_version = tonumber(req_version)
+ if min_version == nil then
+ C.output_dnssim_log():fatal("invalid version number: "..req_version)
+ return nil
+ end
+ if version >= min_version then
+ return version
+ end
+ return nil
+end
+
+-- Check that version of dnssim is at minimum the one passed as
+-- .B req_version
+-- and return the actual version number.
+-- Return nil if the condition is not met.
+--
+-- If no
+-- .B req_version
+-- is specified no check is done and only the version number is returned.
+function DnsSim.check_version(req_version)
+ return _check_version(_DNSSIM_VERSION, req_version)
+end
+
+-- Check that version of dnssim's JSON data format is at minimum the one passed as
+-- .B req_version
+-- and return the actual version number.
+-- Return nil if the condition is not met.
+--
+-- If no
+-- .B req_version
+-- is specified no check is done and only the version number is returned.
+function DnsSim.check_json_version(req_version)
+ return _check_version(_DNSSIM_JSON_VERSION, req_version)
+end
+
+-- Return the Log object to control logging of this instance or module.
+-- Optionally, set the instance's log name.
+-- Unique name should be used for each instance.
+function DnsSim:log(name)
+ if self == nil then
+ return C.output_dnssim_log()
+ end
+ if name ~= nil then
+ C.output_dnssim_log_name(self.obj, name)
+ end
+ return self.obj._log
+end
+
+-- Set the target IPv4/IPv6 address where queries will be sent to.
+function DnsSim:target(ip, port)
+ local nport = tonumber(port)
+ if nport == nil then
+ self.obj._log:fatal("invalid port: "..port)
+ return -1
+ end
+ if nport <= 0 or nport > 65535 then
+ self.obj._log:fatal("invalid port number: "..nport)
+ return -1
+ end
+ return C.output_dnssim_target(self.obj, ip, nport)
+end
+
+-- Specify source IPv4/IPv6 address for sending queries.
+-- Can be set multiple times.
+-- Addresses are selected round-robin when sending.
+function DnsSim:bind(ip)
+ return C.output_dnssim_bind(self.obj, ip)
+end
+
+-- Set the preferred transport to UDP.
+--
+-- When the optional argument
+-- .B tcp_fallback
+-- is set to true, individual queries are re-tried over TCP when TC bit is set in the answer.
+-- Defaults to
+-- .B false
+-- (aka only UDP is used).
+function DnsSim:udp(tcp_fallback)
+ if tcp_fallback == true then
+ C.output_dnssim_set_transport(self.obj, C.OUTPUT_DNSSIM_TRANSPORT_UDP)
+ else
+ C.output_dnssim_set_transport(self.obj, C.OUTPUT_DNSSIM_TRANSPORT_UDP_ONLY)
+ end
+end
+
+-- Set the transport to TCP.
+function DnsSim:tcp()
+ C.output_dnssim_set_transport(self.obj, C.OUTPUT_DNSSIM_TRANSPORT_TCP)
+end
+
+-- Set the transport to TLS.
+--
+-- The optional argument
+-- .B tls_priority
+-- is a GnuTLS priority string, which can be used to select TLS versions, cipher suites etc.
+-- For example:
+--
+-- .RB "- """ NORMAL:%NO_TICKETS """"
+-- will use defaults without TLS session resumption.
+--
+-- .RB "- """ SECURE128:-VERS-ALL:+VERS-TLS1.3 """"
+-- will use only TLS 1.3 with 128-bit secure ciphers.
+--
+-- Refer to:
+-- .I https://gnutls.org/manual/html_node/Priority-Strings.html
+function DnsSim:tls(tls_priority)
+ if tls_priority ~= nil then
+ C.output_dnssim_tls_priority(self.obj, tls_priority)
+ end
+ C.output_dnssim_set_transport(self.obj, C.OUTPUT_DNSSIM_TRANSPORT_TLS)
+end
+
+-- Set the transport to HTTP/2 over TLS.
+--
+-- .B http2_options
+-- is a lua table which supports the following keys:
+--
+-- .B method:
+-- .B GET
+-- (default)
+-- or
+-- .B POST
+--
+-- .B uri_path:
+-- where queries will be sent.
+-- Defaults to
+-- .B /dns-query
+--
+-- .B zero_out_msgid:
+-- when
+-- .B true
+-- (default), query ID is always set to 0
+--
+-- See tls() method for
+-- .B tls_priority
+-- documentation.
+function DnsSim:https2(http2_options, tls_priority)
+ if tls_priority ~= nil then
+ C.output_dnssim_tls_priority(self.obj, tls_priority)
+ end
+
+ uri_path = "/dns-query"
+ zero_out_msgid = true
+ method = "GET"
+
+ if http2_options ~= nil then
+ if type(http2_options) ~= "table" then
+ self.obj._log:fatal("http2_options must be a table")
+ else
+ if http2_options["uri_path"] ~= nil then
+ uri_path = http2_options["uri_path"]
+ end
+ if http2_options["zero_out_msgid"] ~= nil and http2_options["zero_out_msgid"] ~= true then
+ zero_out_msgid = false
+ end
+ if http2_options["method"] ~= nil then
+ method = http2_options["method"]
+ end
+ end
+ end
+
+ C.output_dnssim_set_transport(self.obj, C.OUTPUT_DNSSIM_TRANSPORT_HTTPS2)
+ C.output_dnssim_h2_uri_path(self.obj, uri_path)
+ C.output_dnssim_h2_method(self.obj, method)
+ C.output_dnssim_h2_zero_out_msgid(self.obj, zero_out_msgid)
+end
+
+-- Set timeout for the individual requests in seconds (default 2s).
+--
+-- .BR Beware :
+-- increasing this value while the target resolver isn't very responsive
+-- (cold cache, heavy load) may degrade DnsSim's performance and skew
+-- the results.
+function DnsSim:timeout(seconds)
+ if seconds == nil then
+ seconds = 2
+ end
+ timeout_ms = math.floor(seconds * 1000)
+ C.output_dnssim_timeout_ms(self.obj, timeout_ms)
+end
+
+-- Set TCP connection idle timeout for connection reuse according to RFC7766,
+-- Section 6.2.3 (defaults to 10s).
+-- When set to zero, connections are closed immediately after there are no
+-- more pending queries.
+function DnsSim:idle_timeout(seconds)
+ if seconds == nil then
+ seconds = 10
+ end
+ self.obj.idle_timeout_ms = math.floor(seconds * 1000)
+end
+
+-- Set TCP connection handshake timeout (defaults to 5s).
+-- During heavy load, the server may no longer accept new connections.
+-- This parameter ensures such connection attempts are aborted after the
+-- timeout expires.
+function DnsSim:handshake_timeout(seconds)
+ if seconds == nil then
+ seconds = 5
+ end
+ self.obj.handshake_timeout_ms = math.floor(seconds * 1000)
+end
+
+-- Run the libuv loop once without blocking when there is no I/O.
+-- This should be called repeatedly until 0 is returned and no more data
+-- is expected to be received by DnsSim.
+function DnsSim:run_nowait()
+ return C.output_dnssim_run_nowait(self.obj)
+end
+
+-- Set this to true if DnsSim should free the memory of passed-in objects
+-- (useful when using
+-- .I dnsjit.filter.copy
+-- to pass objects from different thread).
+function DnsSim:free_after_use(free_after_use)
+ self.obj.free_after_use = free_after_use
+end
+
+-- Number of input packets discarded due to various reasons.
+-- To investigate causes, run with increased logging level.
+function DnsSim:discarded()
+ return tonumber(self.obj.discarded)
+end
+
+-- Number of valid requests (input packets) processed.
+function DnsSim:requests()
+ return tonumber(self.obj.stats_sum.requests)
+end
+
+-- Number of requests that received an answer
+function DnsSim:answers()
+ return tonumber(self.obj.stats_sum.answers)
+end
+
+-- Number of requests that received a NOERROR response
+function DnsSim:noerror()
+ return tonumber(self.obj.stats_sum.rcode_noerror)
+end
+
+-- Configure statistics to be collected every N seconds.
+function DnsSim:stats_collect(seconds)
+ if seconds == nil then
+ self.obj._log:fatal("number of seconds must be set for stats_collect()")
+ end
+ interval_ms = math.floor(seconds * 1000)
+ C.output_dnssim_stats_collect(self.obj, interval_ms)
+end
+
+-- Stop the collection of statistics.
+function DnsSim:stats_finish()
+ C.output_dnssim_stats_finish(self.obj)
+end
+
+-- Export the results to a JSON file.
+function DnsSim:export(filename)
+ local file = io.open(filename, "w")
+ if file == nil then
+ self.obj._log:fatal("export failed: no filename")
+ return
+ end
+
+ local function write_stats(file, stats)
+ file:write(
+ "{ ",
+ '"since_ms":', tonumber(stats.since_ms), ',',
+ '"until_ms":', tonumber(stats.until_ms), ',',
+ '"requests":', tonumber(stats.requests), ',',
+ '"ongoing":', tonumber(stats.ongoing), ',',
+ '"answers":', tonumber(stats.answers), ',',
+ '"conn_active":', tonumber(stats.conn_active), ',',
+ '"conn_handshakes":', tonumber(stats.conn_handshakes), ',',
+ '"conn_resumed":', tonumber(stats.conn_resumed), ',',
+ '"conn_handshakes_failed":', tonumber(stats.conn_handshakes_failed), ',',
+ '"rcode_noerror":', tonumber(stats.rcode_noerror), ',',
+ '"rcode_formerr":', tonumber(stats.rcode_formerr), ',',
+ '"rcode_servfail":', tonumber(stats.rcode_servfail), ',',
+ '"rcode_nxdomain":', tonumber(stats.rcode_nxdomain), ',',
+ '"rcode_notimp":', tonumber(stats.rcode_notimp), ',',
+ '"rcode_refused":', tonumber(stats.rcode_refused), ',',
+ '"rcode_yxdomain":', tonumber(stats.rcode_yxdomain), ',',
+ '"rcode_yxrrset":', tonumber(stats.rcode_yxrrset), ',',
+ '"rcode_nxrrset":', tonumber(stats.rcode_nxrrset), ',',
+ '"rcode_notauth":', tonumber(stats.rcode_notauth), ',',
+ '"rcode_notzone":', tonumber(stats.rcode_notzone), ',',
+ '"rcode_badvers":', tonumber(stats.rcode_badvers), ',',
+ '"rcode_badkey":', tonumber(stats.rcode_badkey), ',',
+ '"rcode_badtime":', tonumber(stats.rcode_badtime), ',',
+ '"rcode_badmode":', tonumber(stats.rcode_badmode), ',',
+ '"rcode_badname":', tonumber(stats.rcode_badname), ',',
+ '"rcode_badalg":', tonumber(stats.rcode_badalg), ',',
+ '"rcode_badtrunc":', tonumber(stats.rcode_badtrunc), ',',
+ '"rcode_badcookie":', tonumber(stats.rcode_badcookie), ',',
+ '"rcode_other":', tonumber(stats.rcode_other), ',',
+ '"latency":[')
+ file:write(tonumber(stats.latency[0]))
+ for i=1,tonumber(self.obj.timeout_ms) do
+ file:write(',', tonumber(stats.latency[i]))
+ end
+ file:write("]}")
+ end
+
+ file:write(
+ "{ ",
+ '"version":', _DNSSIM_JSON_VERSION, ',',
+ '"merged":false,',
+ '"stats_interval_ms":', tonumber(self.obj.stats_interval_ms), ',',
+ '"timeout_ms":', tonumber(self.obj.timeout_ms), ',',
+ '"idle_timeout_ms":', tonumber(self.obj.idle_timeout_ms), ',',
+ '"handshake_timeout_ms":', tonumber(self.obj.handshake_timeout_ms), ',',
+ '"discarded":', self:discarded(), ',',
+ '"stats_sum":')
+ write_stats(file, self.obj.stats_sum)
+ file:write(
+ ',',
+ '"stats_periodic":[')
+
+ local stats = self.obj.stats_first
+ write_stats(file, stats)
+
+ while (stats.next ~= nil) do
+ stats = stats.next
+ file:write(',')
+ write_stats(file, stats)
+ end
+
+ file:write(']}')
+ file:close()
+ self.obj._log:notice("results exported to "..filename)
+end
+
+-- Return the C function and context for receiving objects.
+-- Only
+-- .I dnsjit.filter.core.object.ip
+-- or
+-- .I dnsjit.filter.core.object.ip6
+-- objects are supported.
+-- The component expects a 32bit integer (in host order) ranging from 0
+-- to max_clients written to first 4 bytes of destination IP.
+-- See
+-- .IR dnsjit.filter.ipsplit .
+function DnsSim:receive()
+ local receive = C.output_dnssim_receiver()
+ return receive, self.obj
+end
+
+-- Deprecated: use udp() instead.
+--
+-- Set the transport to UDP (without any TCP fallback).
+function DnsSim:udp_only()
+ C.output_dnssim_set_transport(self.obj, C.OUTPUT_DNSSIM_TRANSPORT_UDP_ONLY)
+end
+
+-- dnsjit.filter.copy (3),
+-- dnsjit.filter.ipsplit (3),
+-- dnsjit.filter.core.object.ip (3),
+-- dnsjit.filter.core.object.ip6 (3),
+-- https://gitlab.nic.cz/knot/shotgun
+return DnsSim