Avahi + D-BusでマルチキャストDNS+DNS-SD
もう半年も過ぎてしまいましたが、多重化・負荷分散の自動化の実現に向けて開発中です。
このほどマルチキャストDNS(mDNS)+DNS-SDの実装の一つであるAvahiを使って、サービスの発見とパブリッシュをテストしてみたので、その途中経過をメモ。
mDNSは、DNSをマルチキャストでやりくりしようというもの。クライアント・サーバー型では無いので、冗長化などを考えなくてもいい。それからpull型だけではなくpush型で通知ができるので、DNSレコードの変更をすぐに伝播させられます。
DNS-SDは、DNSのSRVレコードや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にもRubyのD-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