mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-10-05 08:16:55 +08:00
400 lines
8.6 KiB
Go
400 lines
8.6 KiB
Go
package mdns
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/miekg/dns" // awesome library for parsing mDNS records
|
|
)
|
|
|
|
const ServiceHAP = "_hap._tcp.local." // HomeKit Accessory Protocol
|
|
|
|
type ServiceEntry struct {
|
|
Name string `json:"name,omitempty"`
|
|
IP net.IP `json:"ip,omitempty"`
|
|
Port uint16 `json:"port,omitempty"`
|
|
Info map[string]string `json:"info,omitempty"`
|
|
}
|
|
|
|
func (e *ServiceEntry) String() string {
|
|
b, err := json.Marshal(e)
|
|
if err != nil {
|
|
return err.Error()
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func (e *ServiceEntry) TXT() []string {
|
|
var txt []string
|
|
for k, v := range e.Info {
|
|
txt = append(txt, k+"="+v)
|
|
}
|
|
return txt
|
|
}
|
|
|
|
func (e *ServiceEntry) Complete() bool {
|
|
return e.IP != nil && e.Port > 0 && e.Info != nil
|
|
}
|
|
|
|
func (e *ServiceEntry) Addr() string {
|
|
return fmt.Sprintf("%s:%d", e.IP, e.Port)
|
|
}
|
|
|
|
func (e *ServiceEntry) Host(service string) string {
|
|
return e.name() + "." + strings.TrimRight(service, ".")
|
|
}
|
|
|
|
func (e *ServiceEntry) name() string {
|
|
b := []byte(e.Name)
|
|
for i, c := range b {
|
|
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' {
|
|
continue
|
|
}
|
|
b[i] = '-'
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
var MulticastAddr = &net.UDPAddr{
|
|
IP: net.IP{224, 0, 0, 251},
|
|
Port: 5353,
|
|
}
|
|
|
|
const sendTimeout = time.Millisecond * 505
|
|
const respTimeout = time.Second * 3
|
|
|
|
// BasicDiscovery - default golang Multicast UDP listener.
|
|
// Does not work well with multiple interfaces.
|
|
func BasicDiscovery(service string, onentry func(*ServiceEntry) bool) error {
|
|
conn, err := net.ListenMulticastUDP("udp4", nil, MulticastAddr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b := Browser{
|
|
Service: service,
|
|
Addr: MulticastAddr,
|
|
Recv: conn,
|
|
Sends: []net.PacketConn{conn},
|
|
RecvTimeout: respTimeout,
|
|
SendTimeout: sendTimeout,
|
|
}
|
|
|
|
defer b.Close()
|
|
|
|
return b.Browse(onentry)
|
|
}
|
|
|
|
// Discovery - better discovery version. Works well with multiple interfaces.
|
|
func Discovery(service string, onentry func(*ServiceEntry) bool) error {
|
|
b := Browser{
|
|
Service: service,
|
|
Addr: MulticastAddr,
|
|
RecvTimeout: respTimeout,
|
|
SendTimeout: sendTimeout,
|
|
}
|
|
|
|
if err := b.ListenMulticastUDP(); err != nil {
|
|
return err
|
|
}
|
|
|
|
defer b.Close()
|
|
|
|
return b.Browse(onentry)
|
|
}
|
|
|
|
// Query - direct Discovery request on device IP-address. Works even over VPN.
|
|
func Query(host, service string) (entry *ServiceEntry, err error) {
|
|
conn, err := net.ListenPacket("udp4", ":0") // shouldn't use ":5353"
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
br := Browser{
|
|
Service: service,
|
|
Addr: &net.UDPAddr{
|
|
IP: net.ParseIP(host),
|
|
Port: 5353,
|
|
},
|
|
Recv: conn,
|
|
Sends: []net.PacketConn{conn},
|
|
SendTimeout: time.Millisecond * 255,
|
|
RecvTimeout: time.Second,
|
|
}
|
|
|
|
defer br.Close()
|
|
|
|
err = br.Browse(func(en *ServiceEntry) bool {
|
|
entry = en
|
|
return true
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
// QueryOrDiscovery - useful if we know previous device host and want
|
|
// to update port or any other information. Will work even over VPN.
|
|
func QueryOrDiscovery(host, service string, onentry func(*ServiceEntry) bool) error {
|
|
entry, _ := Query(host, service)
|
|
if entry != nil && onentry(entry) {
|
|
return nil
|
|
}
|
|
|
|
return Discovery(service, onentry)
|
|
}
|
|
|
|
type Browser struct {
|
|
Service string
|
|
|
|
Addr net.Addr
|
|
Recv net.PacketConn
|
|
Sends []net.PacketConn
|
|
|
|
RecvTimeout time.Duration
|
|
SendTimeout time.Duration
|
|
}
|
|
|
|
// ListenMulticastUDP - creates multiple senders socket (each for IP4 interface).
|
|
// And one receiver with multicast membership for each sender.
|
|
// Receiver will get multicast responses on senders requests.
|
|
func (b *Browser) ListenMulticastUDP() error {
|
|
// 1. Collect IPv4 interfaces
|
|
ip4s, err := InterfacesIP4()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 2. Create senders
|
|
lc1 := net.ListenConfig{
|
|
Control: func(network, address string, c syscall.RawConn) error {
|
|
return c.Control(func(fd uintptr) {
|
|
// 1. Allow multicast UDP to listen concurrently across multiple listeners
|
|
_ = SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
|
|
})
|
|
},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
for _, ip4 := range ip4s {
|
|
conn, err := lc1.ListenPacket(ctx, "udp4", ip4.String()+":5353") // same port important
|
|
if err != nil {
|
|
continue
|
|
}
|
|
b.Sends = append(b.Sends, conn)
|
|
}
|
|
|
|
if b.Sends == nil {
|
|
return errors.New("no interfaces for listen")
|
|
}
|
|
|
|
// 3. Create receiver
|
|
lc2 := net.ListenConfig{
|
|
Control: func(network, address string, c syscall.RawConn) error {
|
|
return c.Control(func(fd uintptr) {
|
|
// 1. Allow multicast UDP to listen concurrently across multiple listeners
|
|
_ = SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
|
|
|
|
// 2. Disable loop responses
|
|
_ = SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_MULTICAST_LOOP, 0)
|
|
|
|
// 3. Allow receive multicast responses on all this addresses
|
|
mreq := &syscall.IPMreq{
|
|
Multiaddr: [4]byte{224, 0, 0, 251},
|
|
}
|
|
_ = SetsockoptIPMreq(fd, syscall.IPPROTO_IP, syscall.IP_ADD_MEMBERSHIP, mreq)
|
|
|
|
for _, send := range b.Sends {
|
|
addr := send.LocalAddr().(*net.UDPAddr)
|
|
mreq.Interface = [4]byte(addr.IP.To4())
|
|
_ = SetsockoptIPMreq(fd, syscall.IPPROTO_IP, syscall.IP_ADD_MEMBERSHIP, mreq)
|
|
}
|
|
})
|
|
},
|
|
}
|
|
|
|
b.Recv, err = lc2.ListenPacket(ctx, "udp4", ":5353")
|
|
|
|
return err
|
|
}
|
|
|
|
func (b *Browser) Browse(onentry func(*ServiceEntry) bool) error {
|
|
msg := &dns.Msg{
|
|
Question: []dns.Question{
|
|
{Name: b.Service, Qtype: dns.TypePTR, Qclass: dns.ClassINET},
|
|
},
|
|
}
|
|
|
|
query, err := msg.Pack()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = b.Recv.SetDeadline(time.Now().Add(b.RecvTimeout)); err != nil {
|
|
return err
|
|
}
|
|
|
|
go func() {
|
|
for {
|
|
for _, send := range b.Sends {
|
|
if _, err := send.WriteTo(query, b.Addr); err != nil {
|
|
return
|
|
}
|
|
}
|
|
time.Sleep(b.SendTimeout)
|
|
}
|
|
}()
|
|
|
|
processed := map[string]struct{}{"": {}}
|
|
|
|
b2 := make([]byte, 1500)
|
|
for {
|
|
// in the Hass docker network can receive same msg from different address
|
|
n, addr, err := b.Recv.ReadFrom(b2)
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
if err = msg.Unpack(b2[:n]); err != nil {
|
|
continue
|
|
}
|
|
|
|
ptr := GetPTR(msg, b.Service)
|
|
|
|
if _, ok := processed[ptr]; ok {
|
|
continue
|
|
}
|
|
|
|
ip := addr.(*net.UDPAddr).IP
|
|
|
|
for _, entry := range NewServiceEntries(msg, ip) {
|
|
if onentry(entry) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
processed[ptr] = struct{}{}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *Browser) Close() error {
|
|
if b.Recv != nil {
|
|
_ = b.Recv.Close()
|
|
}
|
|
for _, send := range b.Sends {
|
|
_ = send.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func GetPTR(msg *dns.Msg, service string) string {
|
|
for _, record := range msg.Answer {
|
|
if ptr, ok := record.(*dns.PTR); ok && ptr.Hdr.Name == service {
|
|
return ptr.Ptr
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func NewServiceEntries(msg *dns.Msg, ip net.IP) (entries []*ServiceEntry) {
|
|
records := make([]dns.RR, 0, len(msg.Answer)+len(msg.Ns)+len(msg.Extra))
|
|
records = append(records, msg.Answer...)
|
|
records = append(records, msg.Ns...)
|
|
records = append(records, msg.Extra...)
|
|
|
|
// PTR ptr=SomeName._hap._tcp.local. hdr=_hap._tcp.local.
|
|
// TXT txt=... hdr=SomeName._hap._tcp.local.
|
|
// SRV target=SomeName.local. hdr=SomeName._hap._tcp.local.
|
|
// A a=192.168.1.123 hdr=SomeName.local.
|
|
|
|
for _, record := range records {
|
|
ptr, ok := record.(*dns.PTR)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
entry := &ServiceEntry{}
|
|
|
|
if i := strings.IndexByte(ptr.Ptr, '.'); i > 0 {
|
|
entry.Name = strings.ReplaceAll(ptr.Ptr[:i], `\ `, " ")
|
|
}
|
|
|
|
var txt *dns.TXT
|
|
var srv *dns.SRV
|
|
var a *dns.A
|
|
|
|
for _, record = range records {
|
|
if txt, ok = record.(*dns.TXT); ok && txt.Hdr.Name == ptr.Ptr {
|
|
entry.Info = make(map[string]string, len(txt.Txt))
|
|
for _, s := range txt.Txt {
|
|
k, v, _ := strings.Cut(s, "=")
|
|
entry.Info[k] = v
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
for _, record = range records {
|
|
if srv, ok = record.(*dns.SRV); ok && srv.Hdr.Name == ptr.Ptr {
|
|
entry.Port = srv.Port
|
|
|
|
for _, record = range records {
|
|
if a, ok = record.(*dns.A); ok && a.Hdr.Name == srv.Target {
|
|
// device can send multiple IP addresses (ex. Homebridge)
|
|
// use first IP from the list or same IP from sender
|
|
if entry.IP == nil || ip.Equal(a.A) {
|
|
entry.IP = a.A
|
|
}
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
entries = append(entries, entry)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func InterfacesIP4() ([]net.IP, error) {
|
|
intfs, err := net.Interfaces()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var ips []net.IP
|
|
|
|
loop:
|
|
for _, intf := range intfs {
|
|
if intf.Flags&net.FlagUp == 0 || intf.Flags&net.FlagLoopback != 0 {
|
|
continue
|
|
}
|
|
|
|
addrs, err := intf.Addrs()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
for _, addr := range addrs {
|
|
switch v := addr.(type) {
|
|
case *net.IPNet:
|
|
if ip := v.IP.To4(); ip != nil {
|
|
ips = append(ips, ip)
|
|
continue loop
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ips, nil
|
|
}
|