bitcoin_dns_seed 3.33 KB
#!/usr/bin/env ruby
$:.unshift(File.dirname(__FILE__) + "/../lib")

require 'bitcoin'
require 'socket'
require 'json'
require 'rubydns'
require 'optparse'

Bitcoin::Network::Node # autoload to include Array#weighted_sample

DNS_OPTS = {
  :name => nil,
  :connect => "127.0.0.1:9999",
  :port => 8333,
  :cache => 1024,
  :send => 16,
  :interval => 10,
  :ttl => nil,
  :hosts => nil,
}
optparse = OptionParser.new do|opts|
  opts.banner = "Usage: bitcoin_dns_seed [options] <domain>"

  opts.on("-c", "--connect [HOST:PORT]",
    "Connect to server (default: 127.0.0.1:9999)") do |connect|
    DNS_OPTS[:connect] = connect
  end

  opts.on("-p", "--port [PORT]",
    "Network default port each addr must match (default: 8333)") do |port|
    DNS_OPTS[:port] = port.to_i
  end

  opts.on("-c", "--cache [SIZE]", "Cache addresses (default: 1024)") do |size|
    DNS_OPTS[:cache] = size.to_i
  end

  opts.on("-s", "--send [SIZE]", "Send addresses (default: 16)") do |size|
    DNS_OPTS[:send] = size.to_i
  end

  opts.on("-i", "--interval [SECONDS]", "Refresh addresses (default: 10)") do |s|
    DNS_OPTS[:interval] = s.to_i
  end

  opts.on("-t", "--ttl [SECONDS]", "TTL to set on served records (default: relative to addr age)") do |t|
    DNS_OPTS[:ttl] = t.to_i
  end

  opts.on("--hosts [HOSTS]", "Additional hosts file with name => addr mappings") do |h|
    DNS_OPTS[:hosts] = h
  end

  opts.on( '-h', '--help', 'Display this screen' ) do
    puts opts
    exit
  end
end
optparse.parse!

DNS_OPTS[:domain] = ARGV.shift
unless DNS_OPTS[:domain]
  puts optparse
  exit
end


class RubyDNS::Server
  attr_accessor :addrs
end

class AddrFetcher < EM::Connection
  def initialize(dns)
    @dns = dns
    @dns.addrs ||= []
    @buf = BufferedTokenizer.new("\x00")
    send_data(["addrs", DNS_OPTS[:cache].to_s].to_json)
  end

  def receive_data(data)
    @buf.extract(data).each do |packet|
      json = JSON.load(packet)[1]
      @dns.logger.info { "received #{json.size} new addrs" }
      @dns.addrs += json.select{|a| a[1] == DNS_OPTS[:port]}
      @dns.addrs.uniq! {|a| a[0]}
      pop = @dns.addrs.size - DNS_OPTS[:cache]
      @dns.addrs.pop(pop)  if pop > 0
      @dns.logger.debug { "addr pool size now #{@dns.addrs.size}" }
      close_connection
    end
  end

  def self.run(host, port, dns, interval = 30)
    EM.connect(host, port, self, dns)
    EM.add_periodic_timer(interval) do
      EM.connect(host, port, self, dns)
    end
  end
end

dns = RubyDNS::Server.new
dns.match(DNS_OPTS[:domain], :A) do |transaction|
  addrs = dns.addrs.weighted_sample(DNS_OPTS[:send]) {|addr| 10800 - addr[2] }.uniq
  addrs.each do |addr|
    begin
      ttl = DNS_OPTS[:ttl] || ((10800 - addr[2]) / 30)
      transaction.respond!(addr[0], :ttl => ttl)
    rescue
      # ignore faulty addrs ("localhost" etc)
    end
  end
end

if DNS_OPTS[:hosts]
  dns.logger.info { "serving additional addrs from file" }
  File.read(DNS_OPTS[:hosts]).each_line do |line|
    dns.logger.debug { line.strip }
    addr, *names = line.split(/\s+/)
    names.each {|n| dns.match(n, :A) {|t| t.respond!(addr) } }
  end
end

EM.run do
  EventMachine.open_datagram_socket("0.0.0.0", 53, RubyDNS::UDPHandler, dns)
  EventMachine.start_server("0.0.0.0", 53, RubyDNS::TCPHandler, dns)
  dns.logger.info { "DNS Server listening on 0.0.0.0:53" }

  timer = AddrFetcher.run(*DNS_OPTS[:connect].split(":"), dns, DNS_OPTS[:interval])
end