module Sparoid

Single Packet Authorisation client

Constants

SPAROID_CACHE_PATH
VERSION

Public Instance Methods

auth(key, hmac_key, host, port) click to toggle source

Send an authorization packet

# File lib/sparoid.rb, line 15
def auth(key, hmac_key, host, port)
  ips = Resolv.getaddresses(host)
  raise(Error, "Sparoid failed to resolv #{host}") if ips.empty?

  msg = message(cached_public_ip)
  data = prefix_hmac(hmac_key, encrypt(key, msg))
  sendmsg(ips, port, data)

  # wait some time for the server to actually open the port
  # if we don't wait the next SYN package will be dropped
  # and it have to be redelivered, adding 1 second delay
  sleep 0.02
end
fdpass(host, port, connect_timeout: 20) click to toggle source

Connect to a TCP server and pass the FD to the parent

# File lib/sparoid.rb, line 39
def fdpass(host, port, connect_timeout: 20)
  tcp = Socket.tcp host, port, connect_timeout: connect_timeout
  parent = Socket.for_fd(1)
  parent.sendmsg "\0", 0, nil, Socket::AncillaryData.unix_rights(tcp)
end
keygen() click to toggle source

Generate new aes and hmac keys, print to stdout

# File lib/sparoid.rb, line 30
def keygen
  cipher = OpenSSL::Cipher.new("aes-256-cbc")
  key = cipher.random_key.unpack1("H*")
  hmac_key = OpenSSL::Random.random_bytes(32).unpack1("H*")
  puts "key = #{key}"
  puts "hmac-key = #{hmac_key}"
end

Private Instance Methods

cached_public_ip() click to toggle source
# File lib/sparoid.rb, line 87
def cached_public_ip
  if up_to_date_cache?
    read_cache
  else
    write_cache
  end
rescue StandardError => e
  warn "Sparoid: #{e.inspect}"
  public_ip
end
encrypt(key, data) click to toggle source
# File lib/sparoid.rb, line 58
def encrypt(key, data)
  key = [key].pack("H*") # hexstring to bytes
  raise ArgumentError, "Key must be 32 bytes hex encoded" if key.bytesize != 32

  cipher = OpenSSL::Cipher.new("aes-256-cbc")
  cipher.encrypt
  iv = cipher.random_iv
  cipher.key = key
  cipher.iv = iv
  output = iv
  output << cipher.update(data)
  output << cipher.final
end
message(ip) click to toggle source
# File lib/sparoid.rb, line 80
def message(ip)
  version = 1
  ts = (Time.now.utc.to_f * 1000).floor
  nounce = OpenSSL::Random.random_bytes(16)
  [version, ts, nounce, ip.address].pack("N q> a16 a4")
end
prefix_hmac(hmac_key, data) click to toggle source
# File lib/sparoid.rb, line 72
def prefix_hmac(hmac_key, data)
  hmac_key = [hmac_key].pack("H*") # hexstring to bytes
  raise ArgumentError, "HMAC key must be 32 bytes hex encoded" if hmac_key.bytesize != 32

  hmac = OpenSSL::HMAC.digest("SHA256", hmac_key, data)
  hmac + data
end
public_ip() click to toggle source
# File lib/sparoid.rb, line 126
def public_ip
  Resolv::DNS.open(nameserver: ["208.67.222.222", "208.67.220.220"]) do |dns|
    dns.getresource("myip.opendns.com", Resolv::DNS::Resource::IN::A).address
  end
end
read_cache() click to toggle source
# File lib/sparoid.rb, line 105
def read_cache
  File.open(SPAROID_CACHE_PATH, "r") do |f|
    f.flock(File::LOCK_SH)
    Resolv::IPv4.create f.read
  end
rescue ArgumentError => e
  return write_cache if e.message =~ /cannot interpret as IPv4 address/

  raise e
end
sendmsg(ips, port, data) click to toggle source
# File lib/sparoid.rb, line 47
def sendmsg(ips, port, data)
  UDPSocket.open do |socket|
    ips.each do |ip|
      socket.connect ip, port
      socket.sendmsg data, 0
    rescue StandardError => e
      warn "Sparoid error: #{e.message}"
    end
  end
end
up_to_date_cache?() click to toggle source
# File lib/sparoid.rb, line 98
def up_to_date_cache?
  mtime = File.mtime(SPAROID_CACHE_PATH)
  (Time.now - mtime) <= 60 # cache is valid for 1 min
rescue Errno::ENOENT
  false
end
write_cache() click to toggle source
# File lib/sparoid.rb, line 116
def write_cache
  File.open(SPAROID_CACHE_PATH, File::WRONLY | File::CREAT, 0o0644) do |f|
    f.flock(File::LOCK_EX)
    ip = public_ip
    f.truncate(0)
    f.write ip.to_s
    ip
  end
end