# frozen_string_literal: true # # ronin-dns-proxy - A DNS server and proxy library. # # Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com) # # ronin-dns-proxy is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # ronin-dns-proxy 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with ronin-dns-proxy. If not, see . # require 'ronin/dns/proxy/rule' require 'ronin/support/network/dns' require 'async/dns' module Ronin module DNS module Proxy # # A rule based DNS proxy server. # class Server < Async::DNS::Server # The host the server will listen on. # # @return [String] attr_reader :host # The port the server will listen on. # # @return [Integer] attr_reader :port # The upstream DNS resolver. # # @return [Async::DNS::Resolver] # # @api private attr_reader :resolver # The defined rules for the proxy server. # # @return [Array] # # @api private attr_reader :rules # # Initializes the DNS server. # # @param [String] host # The interface to listen on. # # @param [Integer] port # The local port to listen on. # # @param [Array] nameservers # The upstream DNS server(s) to pass queries to. # # @param [Array<(Symbol, String, String), (Symbol, Regexp, String), (Symbol, Regexp, Proc)>] rules # Optional rules to populate the server with. # # @yield [server] # If a block is given, it will be passed the newly created server. # # @example Initializes a new DNS proxy server: # server = Ronin::DNS::Proxy.new('127.0.0.1', 2346) # server.rule :A, 'example.com', '10.0.0.1' # server.rule :AAAA, 'example.com', 'dead:beef::1' # # @example Initializing a new DNS proxy server with a block: # server = Ronin::DNS::Proxy.new('127.0.0.1', 2346) do |server| # server.rule :A, 'example.com', '10.0.0.1' # server.rule :AAAA, 'example.com', 'dead:beef::1' # end # # @api public # def initialize(host,port, nameservers: Ronin::Support::Network::DNS.nameservers, rules: nil) @host = host @port = port super([[:udp, host, port]]) @resolver = Async::DNS::Resolver.new( nameservers.map { |ip| [:udp, ip, 53] } ) @rules = [] if rules rules.each do |(record_type,name,result)| rule(record_type,name,result) end end yield self if block_given? end # # Adds a rule to the server. # # @param [:A, :AAAA, :ANY, :CNAME, :HINFO, :LOC, :MINFO, :MX, :NS, :PTR, :SOA, :SRV, :TXT, :WKS] record_type # The record type that the rule will match against. # # @param [String, Regexp] name # The record name that the rule will match against. # # @param [String, Array, Symbol, #call] result # The result to respond with. It can be a String, or an Array of # Strings, or an error code: # # * `:NoError` - No error occurred. # * `:FormErr` - The incoming data was not formatted correctly. # * `:ServFail` - The operation caused a server failure (internal error, etc). # * `:NXDomain` - Non-eXistant Domain (domain record does not exist). # * `:NotImp` - The operation requested is not implemented. # * `:Refused` - The operation was refused by the server. # * `:NotAuth` - The server is not authoritive for the zone. # # @yield [type, name, transaction] # If no result argument is given, the given block will be passed the # DNS query's type, name, and transaction object. # # @yieldparam [Symbol] type # The query type. # # @yieldparam [String] name # The queried host name. # # @yieldparam [Async::DNS::Transaction] transaction # The DNS query transaction object. # # @example override the IP address for a domain: # server.rule :A, 'example.com', '10.0.0.42' # # @example return multiple IP addresses: # server.rule :A, 'example.com', ['10.0.0.42', '10.0.0.43'] # # @example return an error for the given hostname: # server.rule :A, 'updates.example.com', :ServFail # # @example match a query using a regex: # server.rule :TXT, /^spf\./, "v=spf1 include:10.0.0.1 ~all" # # @example define a dynamic rule: # server.rule(:CNAME, /^www\./) do |type,name,transaction| # # append '.hax' to the domain name # names = name.split('.').push('hax') # # transaction.respond!(names) # end # # @api public # def rule(record_type,name,result=nil,&block) unless (result || block) raise(ArgumentError,"must specify a result value or a block") end @rules << Rule.new(record_type,name,result,&block) end # Mapping of Resolv resource classes to Symbols. # # @api private RECORD_TYPES = { Resolv::DNS::Resource::IN::A => :A, Resolv::DNS::Resource::IN::AAAA => :AAAA, Resolv::DNS::Resource::IN::ANY => :ANY, Resolv::DNS::Resource::IN::CNAME => :CNAME, Resolv::DNS::Resource::IN::HINFO => :HINFO, Resolv::DNS::Resource::IN::LOC => :LOC, Resolv::DNS::Resource::IN::MINFO => :MINFO, Resolv::DNS::Resource::IN::MX => :MX, Resolv::DNS::Resource::IN::NS => :NS, Resolv::DNS::Resource::IN::PTR => :PTR, Resolv::DNS::Resource::IN::SOA => :SOA, Resolv::DNS::Resource::IN::SRV => :SRV, Resolv::DNS::Resource::IN::TXT => :TXT, Resolv::DNS::Resource::IN::WKS => :WKS } # # Processes a received query. # # @param [String] name # The query value (ex: `www.example.com`). # # @param [Class] resource_class # The resource class (ex: `Resolv::DNS::Resource::IN::A`). # # @param [Async::DNS::Transaction] transaction # The DNS transaction object. # # @api private # def process(name,resource_class,transaction) query_type = RECORD_TYPES.fetch(resource_class) matched_rule = @rules.find do |rule| rule.matches?(query_type,name) end if matched_rule matched_rule.call(query_type,name,transaction) else transaction.passthrough!(@resolver) end end end end end end