Path: blob/master/lib/msf/ui/console/command_dispatcher/dns.rb
19664 views
# -*- coding: binary -*-12module Msf3module Ui4module Console5module CommandDispatcher67class DNS89include Msf::Ui::Console::CommandDispatcher1011ADD_USAGE = 'dns [add] [--index <insertion index>] [--rule <wildcard DNS entry>] [--session <session id>] <resolver> ...'.freeze12@@add_opts = Rex::Parser::Arguments.new(13['-i', '--index'] => [true, 'Index to insert at'],14['-r', '--rule'] => [true, 'Set a DNS wildcard entry to match against'],15['-s', '--session'] => [true, 'Force the DNS request to occur over a particular channel (override routing rules)']16)1718ADD_STATIC_USAGE = 'dns [add-static] <hostname> <IP address> ...'.freeze1920REMOVE_USAGE = 'dns [remove/del] -i <entry id> [-i <entry id> ...]'.freeze21@@remove_opts = Rex::Parser::Arguments.new(22['-i', '--index'] => [true, 'Index to remove at']23)2425REMOVE_STATIC_USAGE = 'dns [remove-static] <hostname> [<IP address> ...]'.freeze2627RESET_CONFIG_USAGE = 'dns [reset-config] [-y/--yes] [--system]'.freeze28@@reset_config_opts = Rex::Parser::Arguments.new(29['-y', '--yes'] => [false, 'Assume yes and do not prompt for confirmation before resetting'],30['--system'] => [false, 'Include the system resolver']31)3233RESOLVE_USAGE = 'dns [resolve] [-f <address family>] <hostname> ...'.freeze34@@resolve_opts = Rex::Parser::Arguments.new(35# same usage syntax as Rex::Post::Meterpreter::Ui::Console::CommandDispatcher::Stdapi36['-f'] => [true, 'Address family - IPv4 or IPv6 (default IPv4)']37)3839REORDER_USAGE = 'dns [reorder] -i <existing ID> <new ID>'.freeze40@@reorder_opts = Rex::Parser::Arguments.new(41['-i', '--index'] => [true, 'Index of the rule to move']42)4344def initialize(driver)45super46end4748def name49'DNS'50end5152def commands53commands = {}5455if framework.features.enabled?(Msf::FeatureManager::DNS)56commands = {57'dns' => "Manage Metasploit's DNS resolving behaviour"58}59end60commands61end6263#64# Tab completion for the dns command65#66# @param str [String] the string currently being typed before tab was hit67# @param words [Array<String>] the previously completed words on the command line. The array68# contains at least one entry when tab completion has reached this stage since the command itself has been completed69def cmd_dns_tabs(str, words)70return if driver.framework.dns_resolver.nil?7172subcommands = %w[ add add-static delete flush-cache flush-entries flush-static help print query remove remove-static reset-config resolve ]73if words.length == 174return subcommands.select { |opt| opt.start_with?(str) }75end7677cmd = words[1]78case cmd79when 'add'80# We expect a repeating pattern of tag (e.g. -r) and then a value (e.g. *.metasploit.com)81# Once this pattern is violated, we're just specifying DNS servers at that point.82tag_is_expected = true83if words.length > 284words[2..-1].each do |word|85if tag_is_expected && !word.start_with?('-')86return87end88tag_is_expected = !tag_is_expected89end90end9192case words[-1]93when '-r', '--rule'94# Hard to auto-complete a rule with any meaningful value; just return95return96when '-s', '--session'97session_ids = driver.framework.sessions.keys.map { |k| k.to_s }98return session_ids.select { |id| id.start_with?(str) }99when /^-/100# Unknown tag101return102end103104options = @@add_opts.option_keys.select { |opt| opt.start_with?(str) }105options << '' # Prevent tab-completion of a dash, given they could provide an IP address at this point106return options107when 'add-static'108if words.length == 2109# tab complete existing hostnames because they can have more than one IP address110return resolver.static_hostnames.each.select { |hostname,_| hostname.downcase.start_with?(str.downcase) }.map { |hostname,_| hostname }111end112when 'help'113# These commands don't have any arguments114return subcommands.select { |sc| sc.start_with?(str) }115when 'remove','delete','reorder'116if words[-1] == '-i'117return118else119return @@remove_opts.option_keys.select { |opt| opt.start_with?(str) }120end121when 'remove-static'122if words.length == 2123return resolver.static_hostnames.each.select { |hostname,_| hostname.downcase.start_with?(str.downcase) }.map { |hostname,_| hostname }124elsif words.length > 2125hostname = words[2]126ip_addresses = resolver.static_hostnames.get(hostname, Dnsruby::Types::A) + resolver.static_hostnames.get(hostname, Dnsruby::Types::AAAA)127return ip_addresses.map(&:to_s).select { |ip_address| ip_address.start_with?(str) }128end129when 'reset-config'130@@reset_config_opts.option_keys.select { |opt| opt.start_with?(str) }131when 'resolve','query'132if words[-1] == '-f'133families = %w[ IPv4 IPv6 ] # The family argument is case-insensitive134return families.select { |family| family.downcase.start_with?(str.downcase) }135else136@@resolve_opts.option_keys.select { |opt| opt.start_with?(str) }137end138end139end140141def cmd_dns_help(*args)142if args.first.present?143handler = "#{args.first.gsub('-', '_')}_dns"144if respond_to?("#{handler}_help")145# if it is a valid command with dedicated help information146return send("#{handler}_help")147elsif respond_to?(handler)148# if it is a valid command without dedicated help information149print_error("No help menu is available for #{args.first}")150return151else152print_error("Invalid subcommand: #{args.first}")153end154end155156print_line "Manage Metasploit's DNS resolution behaviour"157print_line158print_line "USAGE:"159print_line " #{ADD_USAGE}"160print_line " #{ADD_STATIC_USAGE}"161print_line " #{REMOVE_USAGE}"162print_line " #{REMOVE_STATIC_USAGE}"163print_line " #{REORDER_USAGE}"164print_line " dns [flush-cache]"165print_line " dns [flush-entries]"166print_line " dns [flush-static]"167print_line " dns [print]"168print_line " #{RESET_CONFIG_USAGE}"169print_line " #{RESOLVE_USAGE}"170print_line " dns [help] [subcommand]"171print_line172print_line "SUBCOMMANDS:"173print_line " add - Add a DNS resolution entry to resolve certain domain names through a particular DNS resolver"174print_line " add-static - Add a statically defined hostname"175print_line " flush-cache - Remove all cached DNS answers"176print_line " flush-entries - Remove all configured DNS resolution entries"177print_line " flush-static - Remove all statically defined hostnames"178print_line " print - Show all configured DNS resolution entries"179print_line " remove - Delete one or more DNS resolution entries"180print_line " remove-static - Delete a statically defined hostname"181print_line " reset-config - Reset the DNS configuration"182print_line " resolve - Resolve a hostname"183print_line " reorder - Reorder one or more rules"184print_line185print_line "EXAMPLES:"186print_line " Display help information for the 'add' subcommand"187print_line " dns help add"188print_line189end190191#192# Manage Metasploit's DNS resolution rules193#194def cmd_dns(*args)195if driver.framework.dns_resolver.nil?196print_warning("Run the #{Msf::Ui::Tip.highlight("save")} command and restart the console for this feature configuration to take effect.")197return198end199200args << 'print' if args.length == 0201# Short-circuit help202if args.delete("-h") || args.delete("--help")203subcommand = args.first204if subcommand && respond_to?("#{subcommand.gsub('-', '_')}_dns_help")205# if it is a valid command with dedicated help information206send("#{subcommand.gsub('-', '_')}_dns_help")207else208# otherwise print the top-level help information209cmd_dns_help210end211return212end213214action = args.shift215begin216case action217when "add"218add_dns(*args)219when "add-static"220add_static_dns(*args)221when "flush-entries"222flush_entries_dns223when "flush-cache"224flush_cache_dns225when "flush-static"226flush_static_dns227when "help"228cmd_dns_help(*args)229when "print"230print_dns231when 'reorder'232reorder_dns(*args)233when "remove", "rm", "delete", "del"234remove_dns(*args)235when "remove-static"236remove_static_dns(*args)237when "reset-config"238reset_config_dns(*args)239when "resolve", "query"240resolve_dns(*args)241else242print_error("Invalid command. To view help: dns -h")243end244rescue ::ArgumentError => e245print_error(e.message)246end247end248249def add_dns(*args)250rules = ['*']251first_rule = true252comm = nil253resolvers = []254index = -1255@@add_opts.parse(args) do |opt, idx, val|256unless resolvers.empty? || opt.nil?257raise ::ArgumentError.new("Invalid command near #{opt}")258end259case opt260when '-i', '--index'261raise ::ArgumentError.new("Not a valid index: #{val}") unless val.to_i > 0262263index = val.to_i - 1264when '-r', '--rule'265raise ::ArgumentError.new('No rule specified') if val.nil?266267rules.clear if first_rule # if the user defines even one rule, clear the defaults268first_rule = false269rules << val270when '-s', '--session'271if val.nil?272raise ::ArgumentError.new('No session specified')273end274275unless comm.nil?276raise ::ArgumentError.new('Only one session can be specified')277end278279comm = val280when nil281val = 'black-hole' if val.casecmp?('blackhole')282resolvers << val283else284raise ::ArgumentError.new("Unknown flag: #{opt}")285end286end287288# The remaining args should be the DNS servers289if resolvers.length < 1290raise ::ArgumentError.new('You must specify at least one upstream DNS resolver')291end292293resolvers.each do |resolver|294unless Rex::Proto::DNS::UpstreamRule.valid_resolver?(resolver)295message = "Invalid DNS resolver: #{resolver}."296if (suggestions = Rex::Proto::DNS::UpstreamRule.spell_check_resolver(resolver)).present?297message << " Did you mean #{suggestions.first}?"298end299300raise ::ArgumentError.new(message)301end302end303304comm_obj = nil305306unless comm.nil?307raise ::ArgumentError.new("Not a valid session: #{comm}") unless comm =~ /\A-?[0-9]+\Z/308309comm_obj = driver.framework.sessions.get(comm.to_i)310raise ::ArgumentError.new("Session does not exist: #{comm}") unless comm_obj311raise ::ArgumentError.new("Socket Comm (Session #{comm}) does not implement Rex::Socket::Comm") unless comm_obj.is_a? ::Rex::Socket::Comm312313if resolvers.any? { |resolver| SPECIAL_RESOLVERS.include?(resolver.downcase) }314print_warning("The session argument will be ignored for the system resolver")315end316end317318rules.each_with_index do |rule, offset|319print_warning("DNS rule #{rule} does not contain wildcards, it will not match subdomains") unless rule.include?('*')320driver.framework.dns_resolver.add_upstream_rule(321resolvers,322comm: comm_obj,323wildcard: rule,324index: (index == -1 ? -1 : offset + index)325)326end327328print_good("#{rules.length} DNS #{rules.length > 1 ? 'entries' : 'entry'} added")329end330331def add_dns_help332print_line "USAGE:"333print_line " #{ADD_USAGE}"334print_line @@add_opts.usage335print_line "RESOLVERS:"336print_line " ipv4 / ipv6 address - The IP address of an upstream DNS server to resolve from"337print_line " #{Rex::Proto::DNS::UpstreamResolver::Type::BLACK_HOLE.to_s.ljust(19)} - Drop all queries"338print_line " #{Rex::Proto::DNS::UpstreamResolver::Type::STATIC.to_s.ljust(19) } - Reply with statically configured addresses (only for A/AAAA records)"339print_line " #{Rex::Proto::DNS::UpstreamResolver::Type::SYSTEM.to_s.ljust(19) } - Use the host operating systems DNS resolution functionality (only for A/AAAA records)"340print_line341print_line "EXAMPLES:"342print_line " Set the DNS server(s) to be used for *.metasploit.com to 192.168.1.10"343print_line " dns add --rule *.metasploit.com 192.168.1.10"344print_line345print_line " Add multiple entries at once"346print_line " dns add --rule *.metasploit.com --rule *.google.com 192.168.1.10 192.168.1.11"347print_line348print_line " Set the DNS server(s) to be used for *.metasploit.com to 192.168.1.10, but specifically to go through session 2"349print_line " dns add --session 2 --rule *.metasploit.com 192.168.1.10"350end351352def add_static_dns(*args)353if args.length < 2354raise ::ArgumentError.new('A hostname and IP address must be provided')355end356357hostname = args.shift358if !Rex::Proto::DNS::StaticHostnames.is_valid_hostname?(hostname)359raise ::ArgumentError.new("Invalid hostname: #{hostname}")360end361362ip_addresses = args363if (ip_address = ip_addresses.find { |a| !Rex::Socket.is_ip_addr?(a) })364raise ::ArgumentError.new("Invalid IP address: #{ip_address}")365end366367ip_addresses.each do |ip_address|368resolver.static_hostnames.add(hostname, ip_address)369print_status("Added static hostname mapping #{hostname} to #{ip_address}")370end371end372373def add_static_dns_help374print_line "USAGE:"375print_line " #{ADD_STATIC_USAGE}"376print_line377print_line "EXAMPLES:"378print_line " Define a static entry mapping localhost6 to ::1"379print_line " dns add-static localhost6 ::1"380end381382#383# Query a hostname using the configuration. This is useful for debugging and384# inspecting the active settings.385#386def resolve_dns(*args)387names = []388query_type = Dnsruby::Types::A389390@@resolve_opts.parse(args) do |opt, idx, val|391unless names.empty? || opt.nil?392raise ::ArgumentError.new("Invalid command near #{opt}")393end394case opt395when '-f'396case val.downcase397when 'ipv4'398query_type = Dnsruby::Types::A399when'ipv6'400query_type = Dnsruby::Types::AAAA401else402raise ::ArgumentError.new("Invalid family: #{val}")403end404when nil405names << val406else407raise ::ArgumentError.new("Unknown flag: #{opt}")408end409end410411if names.length < 1412raise ::ArgumentError.new('You must specify at least one hostname to resolve')413end414415tbl = Table.new(416Table::Style::Default,417'Header' => 'Host resolutions',418'Prefix' => "\n",419'Postfix' => "\n",420'Columns' => ['Hostname', 'IP Address', 'Rule #', 'Rule', 'Resolver', 'Comm channel'],421'ColProps' => { 'Hostname' => { 'Strip' => false } },422'SortIndex' => -1,423'WordWrap' => false424)425names.each do |name|426upstream_rule = resolver.upstream_rules.find { |ur| ur.matches_name?(name) }427if upstream_rule.nil?428tbl << [name, '[Failed To Resolve]', '', '', '', '']429next430end431432upstream_rule_idx = resolver.upstream_rules.index(upstream_rule) + 1433434begin435result = resolver.query(name, query_type)436rescue NoResponseError437tbl = append_resolver_cells!(tbl, upstream_rule, prefix: [name, '[Failed To Resolve]'], index: upstream_rule_idx)438else439if result.answer.empty?440tbl = append_resolver_cells!(tbl, upstream_rule, prefix: [name, '[Failed To Resolve]'], index: upstream_rule_idx)441else442result.answer.select do |answer|443answer.type == query_type444end.map(&:address).map(&:to_s).each do |address|445tbl = append_resolver_cells!(tbl, upstream_rule, prefix: [name, address], index: upstream_rule_idx)446end447end448end449end450print(tbl.to_s)451end452453def resolve_dns_help454print_line "USAGE:"455print_line " #{RESOLVE_USAGE}"456print_line @@resolve_opts.usage457print_line "EXAMPLES:"458print_line " Resolve a hostname to an IPv6 address using the current configuration"459print_line " dns resolve -f IPv6 www.metasploit.com"460print_line461end462463#464# Remove all matching user-configured DNS entries465#466def remove_dns(*args)467remove_ids = []468@@remove_opts.parse(args) do |opt, idx, val|469case opt470when '-i', '--index'471raise ::ArgumentError.new("Not a valid index: #{val}") unless val.to_i > 0472473remove_ids << val.to_i - 1474end475end476477if remove_ids.empty?478raise ::ArgumentError.new('At least one index to remove must be provided')479end480481removed = resolver.remove_ids(remove_ids)482print_warning('Some entries were not removed') unless removed.length == remove_ids.length483if removed.length > 0484print_good("#{removed.length} DNS #{removed.length > 1 ? 'entries' : 'entry'} removed")485print_dns_set('Deleted entries', removed, ids: [nil] * removed.length)486end487end488489def remove_dns_help490print_line "USAGE:"491print_line " #{REMOVE_USAGE}"492print_line(@@remove_opts.usage)493print_line "EXAMPLES:"494print_line " Delete the DNS resolution rule #3"495print_line " dns remove -i 3"496print_line497print_line " Delete multiple rules in one command"498print_line " dns remove -i 3 -i 4 -i 5"499print_line500end501502def reorder_dns(*args)503reorder_ids = []504new_id = -1505@@remove_opts.parse(args) do |opt, idx, val|506case opt507when '-i', '--index'508raise ::ArgumentError.new("Not a valid index: #{val}") unless val.to_i > 0509raise ::ArgumentError.new("Duplicate index: #{val}") if reorder_ids.include?(val.to_i - 1)510511reorder_ids << val.to_i - 1512when nil513raise ::ArgumentError.new("Not a valid index: #{val}") unless (val.to_i > 0 || val.to_i == -1)514new_id = val.to_i515new_id -= 1 unless new_id == -1516end517end518519if reorder_ids.empty?520raise ::ArgumentError.new('At least one index to reorder must be provided')521end522523reordered = resolver.reorder_ids(reorder_ids, new_id)524print_warning('Some entries were not reordered') unless reordered.length == reorder_ids.length525if reordered.length > 0526print_good("#{reordered.length} DNS #{reordered.length > 1 ? 'entries' : 'entry'} reordered")527print_resolver_rules528end529end530531def reorder_dns_help532print_line "USAGE:"533print_line " #{REORDER_USAGE}"534print_line "If providing multiple IDs, they will be inserted at the given index in the order you provide."535print_line(@@reorder_opts.usage)536print_line "EXAMPLES:"537print_line " Move the third DNS entry to the top of the resolution order"538print_line " dns reorder -i 3 1"539print_line540print_line " Move the third and fifth DNS entries just below the first entry (i.e. becoming the second and third entries, respectively)"541print_line " dns reorder -i 3 -i 5 2"542print_line543print_line " Move the second and third DNS entries to the bottom of the resolution order"544print_line " dns reorder -i 2 -i 3 -1"545print_line " Alternatively, assuming there are 6 entries in the list"546print_line " dns reorder -i 2 -i 3 7"547print_line548end549550def remove_static_dns(*args)551if args.length < 1552raise ::ArgumentError.new('A hostname must be provided')553end554555hostname = args.shift556if !Rex::Proto::DNS::StaticHostnames.is_valid_hostname?(hostname)557raise ::ArgumentError.new("Invalid hostname: #{hostname}")558end559560ip_addresses = args561if ip_addresses.empty?562ip_addresses = resolver.static_hostnames.get(hostname, Dnsruby::Types::A) + resolver.static_hostnames.get(hostname, Dnsruby::Types::AAAA)563if ip_addresses.empty?564print_status("There are no definitions for hostname: #{hostname}")565end566elsif (ip_address = ip_addresses.find { |ip| !Rex::Socket.is_ip_addr?(ip) })567raise ::ArgumentError.new("Invalid IP address: #{ip_address}")568end569570ip_addresses.each do |ip_address|571resolver.static_hostnames.delete(hostname, ip_address)572print_status("Removed static hostname mapping #{hostname} to #{ip_address}")573end574end575576def remove_static_dns_help577print_line "USAGE:"578print_line " #{REMOVE_STATIC_USAGE}"579print_line580print_line "EXAMPLES:"581print_line " Remove all IPv4 and IPv6 addresses for 'localhost'"582print_line " dns remove-static localhost"583print_line584end585586def reset_config_dns(*args)587add_system_resolver = false588should_confirm = true589@@reset_config_opts.parse(args) do |opt, idx, val|590case opt591when '--system'592add_system_resolver = true593when '-y', '--yes'594should_confirm = false595end596end597598if should_confirm599print("Are you sure you want to reset the DNS configuration? [y/N]: ")600response = gets.downcase.chomp601return unless response =~ /^y/i602end603604resolver.reinit605print_status('The DNS configuration has been reset')606607if add_system_resolver608# if the user requested that we add the system resolver609system_resolver = Rex::Proto::DNS::UpstreamResolver.create_system610# first find the default, catch-all rule611default_rule = resolver.upstream_rules.find { |ur| ur.matches_all? }612if default_rule.nil?613resolver.add_upstream_rule([ system_resolver ])614else615# if the first resolver is for static hostnames, insert after that one616if default_rule.resolvers.first&.type == Rex::Proto::DNS::UpstreamResolver::Type::STATIC617index = 1618else619index = 0620end621default_rule.resolvers.insert(index, system_resolver)622end623end624625print_dns626627if ENV['PROXYCHAINS_CONF_FILE'] && !add_system_resolver628print_warning('Detected proxychains but the system resolver was not added')629end630end631632def reset_config_dns_help633print_line "USAGE:"634print_line " #{RESET_CONFIG_USAGE}"635print_line @@reset_config_opts.usage636print_line "EXAMPLES:"637print_line " Reset the configuration without prompting to confirm"638print_line " dns reset-config --yes"639print_line640end641642#643# Delete all cached DNS answers644#645def flush_cache_dns646resolver.cache.flush647print_good('DNS cache flushed')648end649650#651# Delete all user-configured DNS settings652#653def flush_entries_dns654resolver.flush655print_good('DNS entries flushed')656end657658def flush_static_dns659resolver.static_hostnames.flush660print_good('DNS static hostnames flushed')661end662663def print_resolver_rules664upstream_rules = resolver.upstream_rules665print_dns_set('Resolver rule entries', upstream_rules, ids: (1..upstream_rules.length).to_a)666if upstream_rules.empty?667print_line668print_error('No DNS nameserver entries configured')669end670end671672#673# Display the user-configured DNS settings674#675def print_dns676default_domain = 'N/A'677if resolver.defname? && resolver.domain.present?678default_domain = resolver.domain679end680print_line("Default search domain: #{default_domain}")681682searchlist = resolver.searchlist683case searchlist.length684when 0685print_line('Default search list: N/A')686when 1687print_line("Default search list: #{searchlist.first}")688else689print_line('Default search list:')690searchlist.each do |entry|691print_line(" * #{entry}")692end693end694print_line("Current cache size: #{resolver.cache.records.length}")695696print_resolver_rules697698tbl = Table.new(699Table::Style::Default,700'Header' => 'Static hostnames',701'Prefix' => "\n",702'Postfix' => "\n",703'Columns' => ['Hostname', 'IPv4 Address', 'IPv6 Address'],704'ColProps' => { 'Hostname' => { 'Strip' => false } },705'SortIndex' => -1,706'WordWrap' => false707)708resolver.static_hostnames.sort_by { |hostname, _| hostname }.each do |hostname, addresses|709ipv4_addresses = addresses.fetch(Dnsruby::Types::A, []).sort_by(&:to_i)710ipv6_addresses = addresses.fetch(Dnsruby::Types::AAAA, []).sort_by(&:to_i)711if (ipv4_addresses.length <= 1 && ipv6_addresses.length <= 1) && ((ipv4_addresses + ipv6_addresses).length > 0)712tbl << [hostname, ipv4_addresses.first, ipv6_addresses.first]713else714tbl << [hostname, '', '']7150.upto([ipv4_addresses.length, ipv6_addresses.length].max - 1) do |idx|716tbl << [TABLE_INDENT, ipv4_addresses[idx], ipv6_addresses[idx]]717end718end719end720print_line(tbl.to_s)721if resolver.static_hostnames.empty?722print_line('No static hostname entries are configured')723end724end725726private727728SPECIAL_RESOLVERS = [729Rex::Proto::DNS::UpstreamResolver::Type::BLACK_HOLE.to_s.downcase,730Rex::Proto::DNS::UpstreamResolver::Type::SYSTEM.to_s.downcase731].freeze732733TABLE_INDENT = " \\_ ".freeze734735#736# Get user-friendly text for displaying the session that this entry would go through737#738def prettify_comm(comm, upstream_resolver)739if !Rex::Socket.is_ip_addr?(upstream_resolver.destination)740'N/A'741elsif comm.nil?742channel = Rex::Socket::SwitchBoard.best_comm(upstream_resolver.destination)743if channel.nil?744nil745else746"Session #{channel.sid} (route)"747end748else749if comm.alive?750"Session #{comm.sid}"751else752"Closed session (#{comm.sid})"753end754end755end756757def print_dns_set(heading, result_set, ids: [])758return if result_set.length == 0759columns = ['#', 'Rule', 'Resolver', 'Comm channel']760col_props = { 'Rule' => { 'Strip' => false } }761762tbl = Table.new(763Table::Style::Default,764'Header' => heading,765'Prefix' => "\n",766'Postfix' => "\n",767'Columns' => columns,768'ColProps' => col_props,769'SortIndex' => -1,770'WordWrap' => false771)772result_set.each_with_index do |entry, index|773tbl = append_resolver_cells!(tbl, entry, index: ids[index])774end775776print(tbl.to_s) if tbl.rows.length > 0777end778779def append_resolver_cells!(tbl, entry, prefix: [], suffix: [], index: nil)780alignment_prefix = prefix.empty? ? [] : (['.'] * prefix.length)781782if entry.resolvers.length == 1783tbl << prefix + [index.to_s, entry.wildcard, entry.resolvers.first, prettify_comm(entry.comm, entry.resolvers.first)] + suffix784elsif entry.resolvers.length > 1785tbl << prefix + [index.to_s, entry.wildcard, '', ''] + suffix786entry.resolvers.each do |resolver|787tbl << alignment_prefix + ['.', TABLE_INDENT, resolver, prettify_comm(entry.comm, resolver)] + ([''] * suffix.length)788end789end790tbl791end792793def resolver794self.driver.framework.dns_resolver795end796end797798end799end800end801end802803804