From f4082426b9dcd71208b5bf953c242d58e4bc229a Mon Sep 17 00:00:00 2001 From: Niten Date: Sun, 15 Nov 2020 20:56:48 -0800 Subject: [PATCH] Initial commit --- .gitignore | 19 ++++ Gemfile | 5 + Rakefile | 12 +++ lib/dns-client.rb | 250 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 286 insertions(+) create mode 100644 .gitignore create mode 100644 Gemfile create mode 100644 Rakefile create mode 100644 lib/dns-client.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e6f6216 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +*.gem +*.rbc +/.config +/coverage/ +/InstalledFiles +/pkg/ +/spec/reports/ +/spec/examples.txt +/test/tmp/ +/test/version_tmp/ +/tmp/ + +## Environment normalization +/.bundle/ +/vendor/bundle +/lib/bundler/man/ + +# Used by RuboCop. Remote config files pulled in from inherit_from directive. +.rubocop-https?--* diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..f75ce5d --- /dev/null +++ b/Gemfile @@ -0,0 +1,5 @@ +source 'https://rubygems.org' + +gem "rake" +gem "xmpp4r" +gem "rubocop" diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..905b1c3 --- /dev/null +++ b/Rakefile @@ -0,0 +1,12 @@ +require "rubocop/rake_task" + +task default: %w[lint] + +task :run do + ruby "lib/dns-client.rb" +end + +RuboCop::RakeTask.new(:lint) do |task| + task.patterns = ['lib/**/*.rb'] + task.fail_on_error = false +end diff --git a/lib/dns-client.rb b/lib/dns-client.rb new file mode 100644 index 0000000..d60f6fb --- /dev/null +++ b/lib/dns-client.rb @@ -0,0 +1,250 @@ + +require "ipaddr" +require "socket" +require "optparse" +require "json" +require "securerandom" + +require "xmpp4r" + +# Jabber::debug = true + +options = {} +OptionParser.new do |opts| + opts.banner = "usage: ${$0} [opts]" + + opts.on("-i", "--interface=INTERFACE", + "Publicly-accessible interface") do |interface| + options[:interface] = interface + end + + opts.on("-d", "--domain=DOMAIN", + "Domain on which we wish to set the new ip") do |domain| + options[:domain] = domain + end + + opts.on("-s", "--server=SERVER", + "Backplane DNS XMPP server") do |server| + options[:server] = server + end + + opts.on("-p", "--password-file=/path/to/file", + "File containing password for XMPP server") do |pw_file| + options[:pw_file] = pw_file + end + + opts.on("-4", "--ipv4", + "Check for a public IPv4 and register with the backplane.") do + options[:ipv4] = true + end + + opts.on("-6", "--ipv6", + "Check for a public IPv6 and register with the backplane.") do + options[:ipv6] = true + end +end.parse! + +def error(msg) + puts msg + throw msg +end + +error("domain is required") if not options[:domain] +error("server is required") if not options[:server] +error("password file is required") if not options[:pw_file] +error("at least one of -4 or -6 required") if not (options[:ipv4] or options[:ipv6]) + +if not File::readable?(options[:pw_file]) + error("file does not exist or is not readable") +end +password = File::open(options[:pw_file]) { |f| f.gets.strip } + +class XMPPClient + def initialize(domain, hostname, server, password) + @jid = "host-#{hostname}@#{server}" + @service_jid = "service-dns@#{server}" + @server = server + @domain = domain + @password = password + @responses = Queue.new + @responses_lock = Mutex.new + end + + def connect + disconnect if connected? + @client = Jabber::Client::new(@jid) + @client.connect # will use SRV records + error("failed to initialize TLS connection") if not @client.is_tls? + @client.auth(@password) + register_response_callback + end + + def connected? + @client ||= nil + @client.respond_to?(:is_connected?) and @client.is_connected? + end + + def disconnect + if @client.respond_to?(:is_connected?) && @client.is_connected? + begin + @client.close + rescue Errno::EPIPE, IOError => e + nil + end + end + @client = nil + end + + def send(msg_content) + msg_id = SecureRandom::uuid + encoded_payload = payload(msg_content, msg_id).to_json + msg = Jabber::Message.new(@service_jid, encoded_payload) + msg.type = :chat + @client.send(msg) + response = receive_response(msg_id) + response and response["status"] == "OK" + end + + def send_ip(ip) + send(ip_payload(ip)) + end + + def payload(req, msg_id) + { + version: 1, + service: :dns, + msgid: msg_id, + payload: req + } + end + + def ip_payload(ip) + { + request: ip.ipv4? ? :change_ipv4 : :change_ipv6, + domain: @domain, + ip: ip.to_s + } + end + + def register_response_callback + @client.add_message_callback do |msg| + enqueue_message(JSON.parse(msg.body)) + end + end + + def enqueue_message(msg) + @responses << msg + end + + def receive_response(msg_id) + msg = @responses.pop + return msg if (msg and (msg["msgid"] == msg_id.to_s)) + raise "failed to receive message: #{msg}" + end +end + +RESERVED_V4_NETWORKS = [ + "0.0.0.0/8", + "10.0.0.0/8", + "100.64.0.0/10", + "127.0.0.0/8", + "169.254.0.0/16", + "172.16.0.0/12", + "192.0.0.0/24", + "192.0.2.0/24", + "192.88.99.0/24", + "192.168.0.0/16", + "198.18.0.0/15", + "198.51.100.0/24", + "203.0.113.0/24", + "224.0.0.0/4", + "240.0.0.0/4", + "255.255.255.255/32" +].map { |ip| IPAddr.new(ip) } + +def public_ip?(ip) + if (ip.ipv4?) + not RESERVED_V4_NETWORKS.any? { |network| network.include? ip } + elsif (ip.ipv6?) + not (ip.link_local? or ip.loopback? or ip.private?) + else + false + end +end + +def to_ipaddr(addrinfo) + if addrinfo.ipv4? + IPAddr.new addrinfo.ip_address + else + IPAddr.new(addrinfo.ip_address.split("%")[0]) + end +end + +def local_addresses + Socket::ip_address_list.map do |addrinfo| + to_ipaddr(addrinfo) + end.select { |ip| public_ip?(ip) } +end + +def interface_addresses(interface) + Socket::getifaddrs.select do |ifaddr| + ifaddr.name == interface + end.select do |ifaddr| + ifaddr.addr.ip? and (ifaddr.flags & Socket::IFF_MULTICAST != 0) + end.map do |ifaddr| + to_ipaddr(ifaddr.addr) + end.filter do |ip| + public_ip? ip + end +end + +client = XMPPClient::new(options[:domain], + Socket::gethostname, + options[:server], + password) + +success = true + +begin + client.connect + + addrs = if options[:interface] + interface_addresses(options[:interface]) + else + local_addresses + end + + if options[:ipv4] + ipv4 = addrs.find { |ip| ip.ipv4? } + if ipv4 + puts "#{options[:server]}: #{Socket::gethostname}.#{options[:domain]} IN A => #{ipv4.to_s}" + if client.send_ip(ipv4) + puts "OK" + else + puts "ERROR" + success = false + end + else + puts "#{options[:server]}: no valid public IPv4 found on the local host" + end + end + + if options[:ipv6] + ipv6 = addrs.find { |ip| ip.ipv6? } + if ipv6 + puts "#{options[:server]}: #{Socket::gethostname}.#{options[:domain]} IN AAAA => #{ipv6.to_s}" + if client.send_ip(ipv6) + puts "OK" + else + puts "ERROR" + success = false + end + else + puts "#{options[:server]}: no valid public IPv6 found on the local host" + end + end +ensure + client.disconnect +end + +exit success ? 0 : 1