install go2rtc on bob

This commit is contained in:
2026-04-04 19:36:14 +02:00
parent f0b56e63d1
commit ccf88187b8
537 changed files with 69213 additions and 0 deletions
@@ -0,0 +1,3 @@
# Useful links
- https://grouper.ieee.org/groups/1722/contributions/2009/Bonjour%20Device%20Discovery.pdf
+374
View File
@@ -0,0 +1,374 @@
package mdns
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"strings"
"syscall"
"time"
"github.com/AlexxIT/go2rtc/pkg/xnet"
"github.com/miekg/dns" // awesome library for parsing mDNS records
)
const (
ServiceDNSSD = "_services._dns-sd._udp.local."
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
Nets []*net.IPNet
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
nets, err := xnet.IPNets(func(ip net.IP) bool {
return !xnet.Docker.Contains(ip)
})
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 _, ipn := range nets {
conn, err := lc1.ListenPacket(ctx, "udp4", ipn.IP.String()+":5353") // same port important
if err != nil {
continue
}
b.Nets = append(b.Nets, ipn)
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
}
@@ -0,0 +1,17 @@
package mdns
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestDiscovery(t *testing.T) {
onentry := func(entry *ServiceEntry) bool {
return true
}
err := Discovery(ServiceHAP, onentry)
//err := Discovery("_ewelink._tcp.local.", time.Second, onentry)
// err := Discovery("_googlecast._tcp.local.", time.Second, onentry)
require.Nil(t, err)
}
+162
View File
@@ -0,0 +1,162 @@
package mdns
import (
"net"
"github.com/miekg/dns"
)
// ClassCacheFlush https://datatracker.ietf.org/doc/html/rfc6762#section-10.2
const ClassCacheFlush = 0x8001
func Serve(service string, entries []*ServiceEntry) error {
b := Browser{Service: service}
if err := b.ListenMulticastUDP(); err != nil {
return err
}
return b.Serve(entries)
}
func (b *Browser) Serve(entries []*ServiceEntry) error {
names := make(map[string]*ServiceEntry, len(entries))
for _, entry := range entries {
name := entry.name() + "." + b.Service
names[name] = entry
}
buf := make([]byte, 1500)
for {
n, addr, err := b.Recv.ReadFrom(buf)
if err != nil {
break
}
var req dns.Msg // request
if err = req.Unpack(buf[:n]); err != nil {
continue
}
// skip messages without Questions
if req.Question == nil {
continue
}
remoteIP := addr.(*net.UDPAddr).IP
localIP := b.MatchLocalIP(remoteIP)
// skip messages from unknown networks (can be docker network)
if localIP == nil {
continue
}
var res dns.Msg // response
for _, q := range req.Question {
if q.Qtype != dns.TypePTR || q.Qclass != dns.ClassINET {
continue
}
if q.Name == ServiceDNSSD {
AppendDNSSD(&res, b.Service)
} else if q.Name == b.Service {
for _, entry := range entries {
AppendEntry(&res, entry, b.Service, localIP)
}
} else if entry, ok := names[q.Name]; ok {
AppendEntry(&res, entry, b.Service, localIP)
}
}
if res.Answer == nil {
continue
}
res.MsgHdr.Response = true
res.MsgHdr.Authoritative = true
data, err := res.Pack()
if err != nil {
continue
}
for _, send := range b.Sends {
_, _ = send.WriteTo(data, MulticastAddr)
}
}
return nil
}
func (b *Browser) MatchLocalIP(remote net.IP) net.IP {
for _, ipn := range b.Nets {
if ipn.Contains(remote) {
return ipn.IP
}
}
return nil
}
func AppendDNSSD(msg *dns.Msg, service string) {
msg.Answer = append(
msg.Answer,
&dns.PTR{
Hdr: dns.RR_Header{
Name: ServiceDNSSD, // _services._dns-sd._udp.local.
Rrtype: dns.TypePTR, // 12
Class: dns.ClassINET, // 1
Ttl: 4500,
},
Ptr: service, // _home-assistant._tcp.local.
},
)
}
func AppendEntry(msg *dns.Msg, entry *ServiceEntry, service string, ip net.IP) {
ptrName := entry.name() + "." + service
srvName := entry.name() + ".local."
msg.Answer = append(
msg.Answer,
&dns.PTR{
Hdr: dns.RR_Header{
Name: service, // _home-assistant._tcp.local.
Rrtype: dns.TypePTR, // 12
Class: dns.ClassINET, // 1
Ttl: 4500,
},
Ptr: ptrName, // Home\ Assistant._home-assistant._tcp.local.
},
)
msg.Extra = append(
msg.Extra,
&dns.TXT{
Hdr: dns.RR_Header{
Name: ptrName, // Home\ Assistant._home-assistant._tcp.local.
Rrtype: dns.TypeTXT, // 16
Class: ClassCacheFlush, // 32769
Ttl: 4500,
},
Txt: entry.TXT(),
},
&dns.SRV{
Hdr: dns.RR_Header{
Name: ptrName, // Home\ Assistant._home-assistant._tcp.local.
Rrtype: dns.TypeSRV, // 33
Class: ClassCacheFlush, // 32769
Ttl: 120,
},
Port: entry.Port, // 8123
Target: srvName, // 963f1fa82b7142809711cebe7c826322.local.
},
&dns.A{
Hdr: dns.RR_Header{
Name: srvName, // 963f1fa82b7142809711cebe7c826322.local.
Rrtype: dns.TypeA, // 1
Class: ClassCacheFlush, // 32769
Ttl: 120,
},
A: ip,
},
)
}
@@ -0,0 +1,15 @@
//go:build !(darwin || ios || freebsd || openbsd || netbsd || dragonfly || windows)
package mdns
import (
"syscall"
)
func SetsockoptInt(fd uintptr, level, opt int, value int) (err error) {
return syscall.SetsockoptInt(int(fd), level, opt, value)
}
func SetsockoptIPMreq(fd uintptr, level, opt int, mreq *syscall.IPMreq) (err error) {
return syscall.SetsockoptIPMreq(int(fd), level, opt, mreq)
}
@@ -0,0 +1,26 @@
//go:build darwin || ios || freebsd || openbsd || netbsd || dragonfly
package mdns
import (
"syscall"
)
func SetsockoptInt(fd uintptr, level, opt int, value int) (err error) {
// change SO_REUSEADDR and REUSEPORT flags simultaneously for BSD-like OS
// https://github.com/AlexxIT/go2rtc/issues/626
// https://stackoverflow.com/questions/14388706/how-do-so-reuseaddr-and-so-reuseport-differ/14388707
if opt == syscall.SO_REUSEADDR {
if err = syscall.SetsockoptInt(int(fd), level, opt, value); err != nil {
return
}
opt = syscall.SO_REUSEPORT
}
return syscall.SetsockoptInt(int(fd), level, opt, value)
}
func SetsockoptIPMreq(fd uintptr, level, opt int, mreq *syscall.IPMreq) (err error) {
return syscall.SetsockoptIPMreq(int(fd), level, opt, mreq)
}
@@ -0,0 +1,13 @@
//go:build windows
package mdns
import "syscall"
func SetsockoptInt(fd uintptr, level, opt int, value int) (err error) {
return syscall.SetsockoptInt(syscall.Handle(fd), level, opt, value)
}
func SetsockoptIPMreq(fd uintptr, level, opt int, mreq *syscall.IPMreq) (err error) {
return syscall.SetsockoptIPMreq(syscall.Handle(fd), level, opt, mreq)
}