Initial commit
This commit is contained in:
commit
f4082426b9
|
@ -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?--*
|
|
@ -0,0 +1,5 @@
|
||||||
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
|
gem "rake"
|
||||||
|
gem "xmpp4r"
|
||||||
|
gem "rubocop"
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue