Avahi + D-BusでマルチキャストDNS+DNS-SD

もう半年も過ぎてしまいましたが、多重化・負荷分散の自動化の実現に向けて開発中です。
このほどマルチキャストDNS(mDNS)+DNS-SDの実装の一つであるAvahiを使って、サービスの発見とパブリッシュをテストしてみたので、その途中経過をメモ。


mDNSは、DNSをマルチキャストでやりくりしようというもの。クライアント・サーバー型では無いので、冗長化などを考えなくてもいい。それからpull型だけではなくpush型で通知ができるので、DNSレコードの変更をすぐに伝播させられます。


DNS-SDは、DNSSRVレコードやTXTレコードにいろいろデータを入れて、サービスの自動発見をやろうというもの(RFC 2782, draft-cheshire-dnsext-dns-sd-04)。SRVレコードに「_nfs._tcp」のようにサービス名とプロトコルを入れて、TXTレコードに「path=/path/to/mount」のような付加情報を入れます。mDNSと組み合わせると非常に有効。
draft-cheshire-dnsext-dns-sd-04によれば、TXTレコードには65535バイトまで入れられるらしいですが、それが普通に使えるなら、結構いろいろ入れられそう。MTUで制限される?


AvahiにもRubyD-Busバインディングruby-dbus)にもバグがあって、ハマりまくりでした…そんな汗と涙の結晶であります。

# Avahi用定数
module Avahi
  ####
  ## address.h
  ##
  PROTO_INET      =  0
  PROTO_INET6     =  1
  PROTO_UNSPEC    = -1

  IF_UNSPEC       = -1


  ####
  ## defs.h
  ##
  # AvahiPublishFlags
  PUBLISH_UNIQUE = 1
  PUBLISH_NO_PROBE = 2
  PUBLISH_NO_ANNOUNCE = 4
  PUBLISH_ALLOW_MULTIPLE = 8
  PUBLISH_NO_REVERSE = 16
  PUBLISH_NO_COOKIE = 32
  PUBLISH_UPDATE = 64
  PUBLISH_USE_WIDE_AREA = 128
  PUBLISH_USE_MULTICAST = 256

  # AvahiLookupFlags
  LOOKUP_USE_WIDE_AREA = 1
  LOOKUP_USE_MULTICAST = 2
  LOOKUP_NO_TXT = 4
  LOOKUP_NO_ADDRESS = 8

  # AvahiLookupResultFlags
  LOOKUP_RESULT_CACHED = 1
  LOOKUP_RESULT_WIDE_AREA = 2
  LOOKUP_RESULT_MULTICAST = 4
  LOOKUP_RESULT_LOCAL = 8
  LOOKUP_RESULT_OUR_OWN = 16
  LOOKUP_RESULT_STATIC = 32

  DNS_TYPE_A = 0x01
  DNS_TYPE_NS = 0x02
  DNS_TYPE_CNAME = 0x05
  DNS_TYPE_SOA = 0x06
  DNS_TYPE_PTR = 0x0C
  DNS_TYPE_HINFO = 0x0D
  DNS_TYPE_MX = 0x0F
  DNS_TYPE_TXT = 0x10
  DNS_TYPE_AAAA = 0x1C
  DNS_TYPE_SRV = 0x2

  DNS_CLASS_IN = 0x01
end


require 'dbus'

module VIVER
  class RUNES
    def initialize(interface)
      @bus = DBus::SystemBus.instance
      @avahi = @bus.service('org.freedesktop.Avahi')
      @server = @avahi.object('/')
      @server.introspect
      @server.default_iface = 'org.freedesktop.Avahi.Server'

      # identify myself
      @ifindex = @server.GetNetworkInterfaceIndexByName(interface)[0]
      @domain = @server.GetDomainName()[0]
      @hostname = @server.GetHostName()[0]
      @hostname_fqdn = @server.GetHostNameFqdn()[0]
      interface, @protocol, name, aprotocol, @address, flags =
          @server.ResolveHostName(Avahi::IF_UNSPEC, Avahi::IF_UNSPEC, @hostname_fqdn, Avahi::IF_UNSPEC, 0)

      entry_path = @server.EntryGroupNew()[0]
      @entry = @avahi.object(entry_path)
      @entry.introspect
      @entry.default_iface = 'org.freedesktop.Avahi.EntryGroup'
    end
    def on_service(service, &callback)
      path = @server.ServiceBrowserNew(
        Avahi::IF_UNSPEC, Avahi::PROTO_UNSPEC, service, @domain, 0)[0]
      browser = @avahi.object(path)
      browser.default_iface = 'org.freedesktop.Avahi.ServiceBrowser'
      browser.introspect
      browser.on_signal('ItemNew') {|interface, protocol, name, type, domain, flags|
        callback.call( resolve(name, type, domain) )
      }
    end
    def resolve(name, type, domain = @domain)
      #interface, protocol, name, type, domain, host, aprotocol, address, port, text, flags =
      @server.ResolveService(@ifindex, Avahi::PROTO_UNSPEC, name, type, domain, Avahi::PROTO_UNSPEC, 0)
    end

    def set_host_name(name)
      @server.SetHostName(name)
      @hostname = @server.GetHostName(name)
    end

    def publish(name, type, port, text, domain = @domain, hostname_fqdn = @hostname_fqdn)
      @entry.AddService(@ifindex, @protocol, 0, name, type, domain, hostname_fqdn, port, text)
      @entry.Commit()
    end

    def loop
      main = DBus::Main.new
      main << @bus
      main.run
    end

  end
end

include VIVER


# ここから本体開始
runes = RUNES.new('eth0')

# _ssh._tcpを探してみる
runes.on_service('_ssh._tcp') {|interface, protocol, name, type, domain, host, aprotocol, address, port, text, flags|
  puts "Service _ssh._tcp: #{host} (#{address}) #{text.inspect}"
}
#=> Service _ssh._tcp: xcore.local (192.168.0.4) []  # これは私のMacBook(Bonjour)
#=> Service _ssh._tcp: vcore.local (192.168.0.2) []  # これは開発用Linuxマシン(Avahiが動作中)

# 新しいDNSレコードをパブリッシュ
runes.publish("VIVER Boot Service", "_viver-boot-service._tcp", 4000, ['gziped config file...'.unpack('C*')])

# パブリッシュしたレコードを自分で探してみる
runes.on_service('_viver-boot-service._tcp') {|interface, protocol, name, type, domain, host, aprotocol, address, port, text, flags|
  puts "Service _viver-boot-service._tcp: #{host} (#{address}) #{text.map{|b|b.pack('c*')}.inspect}"
}
#=> Service _viver-boot-service._tcp: vcore.local (192.168.0.2) ["gziped config file..."]

runes.loop