1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
|
# Copyright, 2009, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
require 'celluloid/io'
require_relative 'transaction'
require_relative 'logger'
module RubyDNS
class UDPSocketWrapper < Celluloid::IO::UDPSocket
def initialize(socket)
@socket = socket
end
end
class TCPServerWrapper < Celluloid::IO::TCPServer
def initialize(server)
@server = server
end
end
class Server
include Celluloid::IO
finalizer :shutdown
# The default server interfaces
DEFAULT_INTERFACES = [[:udp, "0.0.0.0", 53], [:tcp, "0.0.0.0", 53]]
# Instantiate a server with a block
#
# server = Server.new do
# match(/server.mydomain.com/, IN::A) do |transaction|
# transaction.respond!("1.2.3.4")
# end
# end
#
def initialize(options = {})
@handlers = []
@logger = options[:logger] || Celluloid.logger
@interfaces = options[:listen] || DEFAULT_INTERFACES
@origin = options[:origin] || '.'
end
# Records are relative to this origin:
attr_accessor :origin
attr_accessor :logger
# Fire the named event as part of running the server.
def fire(event_name)
end
def shutdown
fire(:stop)
end
# Give a name and a record type, try to match a rule and use it for processing the given arguments.
def process(name, resource_class, transaction)
raise NotImplementedError.new
end
# Process an incoming DNS message. Returns a serialized message to be sent back to the client.
def process_query(query, options = {}, &block)
start_time = Time.now
# Setup response
response = Resolv::DNS::Message::new(query.id)
response.qr = 1 # 0 = Query, 1 = Response
response.opcode = query.opcode # Type of Query; copy from query
response.aa = 1 # Is this an authoritative response: 0 = No, 1 = Yes
response.rd = query.rd # Is Recursion Desired, copied from query
response.ra = 0 # Does name server support recursion: 0 = No, 1 = Yes
response.rcode = 0 # Response code: 0 = No errors
transaction = nil
begin
query.question.each do |question, resource_class|
begin
question = question.without_origin(@origin)
@logger.debug {"<#{query.id}> Processing question #{question} #{resource_class}..."}
transaction = Transaction.new(self, query, question, resource_class, response, options)
transaction.process
rescue Resolv::DNS::OriginError
# This is triggered if the question is not part of the specified @origin:
@logger.debug {"<#{query.id}> Skipping question #{question} #{resource_class} because #{$!}"}
end
end
rescue Celluloid::ResumableError
raise
rescue StandardError => error
@logger.error "<#{query.id}> Exception thrown while processing #{transaction}!"
RubyDNS.log_exception(@logger, error)
response.rcode = Resolv::DNS::RCode::ServFail
end
end_time = Time.now
@logger.debug {"<#{query.id}> Time to process request: #{end_time - start_time}s"}
return response
end
#
# By default the server runs on port 53, both TCP and UDP, which is usually a priviledged port and requires root access to bind. You can change this by specifying `options[:listen]` which should contain an array of `[protocol, interface address, port]` specifications.
#
# INTERFACES = [[:udp, "0.0.0.0", 5300]]
# RubyDNS::run_server(:listen => INTERFACES) do
# ...
# end
#
# You can specify already connected sockets if need be:
#
# socket = UDPSocket.new; socket.bind("0.0.0.0", 53)
# Process::Sys.setuid(server_uid)
# INTERFACES = [socket]
#
def run
@logger.info "Starting RubyDNS server (v#{RubyDNS::VERSION})..."
fire(:setup)
# Setup server sockets
@interfaces.each do |spec|
if spec.is_a?(BasicSocket)
spec.do_not_reverse_lookup
protocol = spec.getsockopt(Socket::SOL_SOCKET, Socket::SO_TYPE).unpack("i")[0]
ip = spec.local_address.ip_address
port = spec.local_address.ip_port
case protocol
when Socket::SOCK_DGRAM
@logger.info "<> Attaching to pre-existing UDP socket #{ip}:#{port}"
link UDPSocketHandler.new(self, UDPSocketWrapper.new(spec))
when Socket::SOCK_STREAM
@logger.info "<> Attaching to pre-existing TCP socket #{ip}:#{port}"
link TCPSocketHandler.new(self, TCPServerWrapper.new(spec))
else
raise ArgumentError.new("Unknown socket protocol: #{protocol}")
end
elsif spec[0] == :udp
@logger.info "<> Listening on #{spec.join(':')}"
link UDPHandler.new(self, spec[1], spec[2])
elsif spec[0] == :tcp
@logger.info "<> Listening on #{spec.join(':')}"
link TCPHandler.new(self, spec[1], spec[2])
else
raise ArgumentError.new("Invalid connection specification: #{spec.inspect}")
end
end
fire(:start)
end
end
# Provides the core of the RubyDNS domain-specific language (DSL). It contains a list of rules which are used to match against incoming DNS questions. These rules are used to generate responses which are either DNS resource records or failures.
class RuleBasedServer < Server
# Represents a single rule in the server.
class Rule
def initialize(pattern, callback)
@pattern = pattern
@callback = callback
end
# Returns true if the name and resource_class are sufficient:
def match(name, resource_class)
# If the pattern doesn't specify any resource classes, we implicitly pass this test:
return true if @pattern.size < 2
# Otherwise, we try to match against some specific resource classes:
if Class === @pattern[1]
@pattern[1] == resource_class
else
@pattern[1].include?(resource_class) rescue false
end
end
# Invoke the rule, if it matches the incoming request, it is evaluated and returns `true`, otherwise returns `false`.
def call(server, name, resource_class, transaction)
unless match(name, resource_class)
server.logger.debug "<#{transaction.query.id}> Resource class #{resource_class} failed to match #{@pattern[1].inspect}!"
return false
end
# Does this rule match against the supplied name?
case @pattern[0]
when Regexp
match_data = @pattern[0].match(name)
if match_data
server.logger.debug "<#{transaction.query.id}> Regexp pattern matched with #{match_data.inspect}."
@callback[transaction, match_data]
return true
end
when String
if @pattern[0] == name
server.logger.debug "<#{transaction.query.id}> String pattern matched."
@callback[transaction]
return true
end
else
if (@pattern[0].call(name, resource_class) rescue false)
server.logger.debug "<#{transaction.query.id}> Callable pattern matched."
@callback[transaction]
return true
end
end
server.logger.debug "<#{transaction.query.id}> No pattern matched."
# We failed to match the pattern.
return false
end
def to_s
@pattern.inspect
end
end
# Don't wrap the block going into initialize.
execute_block_on_receiver :initialize
# Instantiate a server with a block
#
# server = Server.new do
# match(/server.mydomain.com/, IN::A) do |transaction|
# transaction.respond!("1.2.3.4")
# end
# end
#
def initialize(options = {}, &block)
super(options)
@events = {}
@rules = []
@otherwise = nil
if block_given?
instance_eval(&block)
end
end
attr_accessor :logger
# This function connects a pattern with a block. A pattern is either a String or a Regex instance. Optionally, a second argument can be provided which is either a String, Symbol or Array of resource record types which the rule matches against.
#
# match("www.google.com")
# match("gmail.com", IN::MX)
# match(/g?mail.(com|org|net)/, [IN::MX, IN::A])
#
def match(*pattern, &block)
@rules << Rule.new(pattern, block)
end
# Register a named event which may be invoked later using #fire
#
# on(:start) do |server|
# Process::Daemon::Permissions.change_user(RUN_AS)
# end
def on(event_name, &block)
@events[event_name] = block
end
# Fire the named event, which must have been registered using on.
def fire(event_name)
callback = @events[event_name]
if callback
callback.call(self)
end
end
# Specify a default block to execute if all other rules fail to match. This block is typially used to pass the request on to another server (i.e. recursive request).
#
# otherwise do |transaction|
# transaction.passthrough!($R)
# end
#
def otherwise(&block)
@otherwise = block
end
# If you match a rule, but decide within the rule that it isn't the correct one to use, you can call `next!` to evaluate the next rule - in other words, to continue falling down through the list of rules.
def next!
throw :next
end
# Give a name and a record type, try to match a rule and use it for processing the given arguments.
def process(name, resource_class, transaction)
@logger.debug {"<#{transaction.query.id}> Searching for #{name} #{resource_class.name}"}
@rules.each do |rule|
@logger.debug {"<#{transaction.query.id}> Checking rule #{rule}..."}
catch (:next) do
# If the rule returns true, we assume that it was successful and no further rules need to be evaluated.
return if rule.call(self, name, resource_class, transaction)
end
end
if @otherwise
@otherwise.call(transaction)
else
@logger.warn "<#{transaction.query.id}> Failed to handle #{name} #{resource_class.name}!"
end
end
end
end
|