Skip to content

Instantly share code, notes, and snippets.

@tenderlove
Created November 19, 2023 20:02
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save tenderlove/49f330e27a8943cd1a3505ae06e8492c to your computer and use it in GitHub Desktop.
Save tenderlove/49f330e27a8943cd1a3505ae06e8492c to your computer and use it in GitHub Desktop.
Sample mDNS client in Ruby
# mDNS client
#
# Usage: ruby script.rb "_http._tcp.local."
require "socket"
require "ipaddr"
require "fcntl"
require "resolv"
module DNSSD
MDNS_PORT = 5353
MDNS_UNICAST_RESPONSE = 0x8000
class Query
attr_reader :name
TypeValue = 12 # PTR
ClassValue = 1 | MDNS_UNICAST_RESPONSE
def initialize name
@name = name
end
end
def self.browse name, &blk
send_query [Query.new(name)], &blk
end
private_class_method def self.open_client_sockets port
addrs = Socket.getifaddrs
addrs.select { |ifa|
ifa.addr &&
(ifa.flags & Socket::IFF_UP > 0) && # must be up
(ifa.flags & Socket::IFF_MULTICAST > 0) && # must have multicast
(ifa.flags & Socket::IFF_LOOPBACK == 0) && # must not be loopback
(ifa.flags & Socket::IFF_POINTOPOINT == 0) # must not be pointopoint
}.select { |ifa| ifa.addr.ipv4? }.map { |ifa|
open_ipv4(ifa.addr, port)
}
end
private_class_method def self.send_query queries
sockets = open_client_sockets 0
sockets.each { |socket| multiquery_send socket, queries, 0 }
loop do
readers, = IO.select(sockets, [], [])
readers.each do |reader|
yield query_recv(reader)
end
end
end
private_class_method def self.open_ipv4 saddr, port
sock = UDPSocket.new
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true)
sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, true)
sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_LOOP, true)
setup_ipv4 sock, saddr, port
sock
end
private_class_method def self.setup_ipv4 sock, saddr, port
sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP,
IPAddr.new("224.0.0.251").hton + IPAddr.new(saddr.ip_address).hton)
sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_IF, IPAddr.new(saddr.ip_address).hton)
sock.bind saddr.ip_address, port
flags = sock.fcntl(Fcntl::F_GETFL, 0)
sock.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK | flags)
end
private_class_method def self.multiquery_send sock, queries, query_id
query = Resolv::DNS::Message.new query_id
queries.each { |q| query.add_question q.name, q.class }
multicast_send sock, query.encode
end
private_class_method def self.multicast_send sock, query
dest = Addrinfo.new(["AF_INET", MDNS_PORT, nil, "224.0.0.251"])
sock.send(query, 0, dest)
end
private_class_method def self.query_recv sock
buf, = sock.recvfrom 2048
Resolv::DNS::Message.decode(buf)
end
end
if __FILE__ == $0
DNSSD.browse ARGV[0] || "_http._tcp.local." do |recv|
name = nil
address = nil
recv.additional.each do |_, _, data|
case data
when Resolv::DNS::Resource::IN::SRV
name = data.target
when Resolv::DNS::Resource::IN::A
address = data.address
end
end
p FOUND: [name, address]
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment