# frozen_string_literal: true # # Copyright (c) 2006-2023 Hal Brodigan (postmodern.mod3 at gmail.com) # # Ronin 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. # # Ronin 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 Ronin. If not, see . # require 'ronin/cli/command' require 'ronin/dns/proxy' module Ronin class CLI module Commands # # Starts a DNS proxy. # # ## Usage # # ronin dns-proxy [options] [HOST] PORT # # ## Options # # -n, --nameserver IP The upstream nameserver IP to use # -r RECORD_TYPE:NAME:RESULT|RECORD_TYPE:/REGEXP/:RESULT, # --rule Adds a rule to the DNS proxy # -h, --help Print help information # # ## Arguments # # [HOST] The host name to listen on. # PORT The port number to listen on. # # @since 2.1.0 # class DnsProxy < Command usage '[options] [HOST] PORT' option :nameserver, short: '-n', value: { type: String, usage: 'IP' }, desc: 'The upstream nameserver IP to use' do |ip| @nameservers << ip end option :rule, short: '-r', value: { type: %r{\A[^:]+:(?:[^:]+|/[^/:]+/):.+\z}, usage: 'RECORD_TYPE:NAME:RESULT|RECORD_TYPE:/REGEXP/:RESULT' }, desc: 'Adds a rule to the DNS proxy' do |rule| @rules << parse_rule(rule) end argument :host, required: false, desc: 'The host to listen on' argument :port, required: true, desc: 'The port number to listen on' description 'Starts a DNS proxy' man_page 'ronin-dns-proxy.1' # The upstream nameserver IP addresses to forward DNS queries to. # # @return [Array] attr_reader :nameservers # The rules for the DNS proxy server. # # @return [Array<(Symbol, String, String), (Symbol, Regexp, String)>] attr_reader :rules # # Initializes the `ronin dns-proxy` command. # # @param [Hash{Symbol => Object}] kwargs # Additional keyword arguments for the command. # def initialize(**kwargs) super(**kwargs) @nameservers = [] @rules = [] end # # Runs the `ronin dns-proxy` command. # def run(host='127.0.0.1',port) port = port.to_i DNS::Proxy.run(host,port,**proxy_kwargs) end # # The keyword arguments for `Ronin::DNS::Proxy.run`. # # @return [Hash{Symbol => Object}] # def proxy_kwargs kwargs = {rules: @rules} unless @nameservers.empty? kwargs[:nameservers] = @nameservers end return kwargs end # Record types. RECORD_TYPES = { 'A' => :A, 'AAAA' => :AAAA, 'ANY' => :ANY, 'CNAME' => :CNAME, 'HINFO' => :HINFO, 'LOC' => :LOC, 'MINFO' => :MINFO, 'MX' => :MX, 'NS' => :NS, 'PTR' => :PTR, 'SOA' => :SOA, 'SRV' => :SRV, 'TXT' => :TXT, 'WKS' => :WKS } # # Parses a record type name. # # @param [String] record_type # The record type to parse. # # @return [:A, :AAAA, :ANY, :CNAME, :HINFO, :LOC, :MINFO, :MX, :NS, :PTR, :SOA, :SRV, :TXT, :WKS] # The parsed record type. # # @raise [OptionParser::InvalidArgument] # The record type was unknown. # def parse_record_type(record_type) RECORD_TYPES.fetch(record_type) do raise(OptionParser::InvalidArgument,"invalid record type: #{record_type.inspect}") end end # # Parses the name field of a record. # # @param [String] name # The name field to parse. # # @return [String, Regex] # The parsed name. If the name field starts with a `/` and ends with a # `/`, then a Regexp will be returned. # # @raise [OptionParser::InvalidArgument] # The name field regex could not be parsed. # def parse_record_name(name) if name.start_with?('/') && name.end_with?('/') begin Regexp.new(name[1..-2]) rescue RegexpError => error raise(OptionParser::InvalidArgument,"invalid Regexp: #{error.message}") end else name end end # Error names. ERROR_CODES = { 'NoError' => :NoError, 'FormErr' => :FormErr, 'ServFail' => :ServFail, 'NXDomain' => :NXDomain, 'NotImp' => :NotImp, 'Refused' => :Refused, 'NotAuth' => :NotAuth } # # Parses a result value. # # @param [String] result # A result value to parse. # # @return [String, :NoError, :FormErr, :ServFail, :NXDomain, :NotImp, :Refused, :NotAuth] # The parsed result value or a DNS error code. # def parse_rule_result(result) ERROR_CODES.fetch(result,result) end # # Parses a rule string. # # @param [String] rule # The string to parse. # # @return [(Symbol, String, String), (Symbol, Regexp, String)] # The parsed rule. # # @raise [OptionParser::InvalidArgument] # The rule string could not be parsed. # def parse_rule(rule) record_type, name, result = rule.split(':',3) [ parse_record_type(record_type), parse_record_name(name), parse_rule_result(result) ] end end end end end