feat(discovery): android compatibility

This commit is contained in:
wpetit 2024-08-06 09:32:02 +02:00
parent 6842d4d88a
commit 30d466db7d
7 changed files with 295 additions and 80 deletions

View File

@ -5,25 +5,30 @@ import (
"encoding/json"
"fmt"
"os"
"time"
"forge.cadoles.com/cadoles/go-emlid/reach/discovery"
"github.com/pkg/errors"
)
func main() {
services, err := discovery.Watch(context.Background())
resolver := discovery.NewResolver()
found, err := resolver.Scan(context.Background(), 1*time.Second)
if err != nil {
fmt.Printf("[FATAL] %+v", errors.WithStack(err))
os.Exit(1)
}
for srv := range services {
for srv := range found {
data, err := json.MarshalIndent(struct {
Addr string `json:"addr"`
Name string `json:"name"`
Addr string `json:"addr"`
Name string `json:"name"`
Device string `json:"device"`
}{
Name: srv.Name,
Addr: fmt.Sprintf("%s:%d", srv.AddrV4.String(), srv.Port),
Name: srv.Name,
Addr: fmt.Sprintf("%s:%d", srv.AddrV4.String(), srv.Port),
Device: srv.Device,
}, "", " ")
if err != nil {
fmt.Printf("[FATAL] %+v", errors.WithStack(err))

8
go.mod
View File

@ -14,8 +14,10 @@ require (
require (
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/miekg/dns v1.1.27 // indirect
github.com/hashicorp/mdns v1.0.5 // indirect
github.com/miekg/dns v1.1.41 // indirect
github.com/wlynxg/anet v0.0.4-0.20240806025826-e684438fc7c6 // indirect
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa // indirect
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe // indirect
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 // indirect
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 // indirect
)

21
go.sum
View File

@ -12,12 +12,20 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg=
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/wlynxg/anet v0.0.4-0.20240806025826-e684438fc7c6 h1:c/wkXIJvpg2oot7iFqPESTBAO9UvhWTBnW97y9aPgyU=
github.com/wlynxg/anet v0.0.4-0.20240806025826-e684438fc7c6/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -27,12 +35,25 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 h1:4qWs8cYYH6PoEFy4dfhDFgoMGkwAcETd+MmPdCPMzUc=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -1,68 +0,0 @@
package discovery
import (
"context"
"net"
"github.com/grandcat/zeroconf"
"github.com/pkg/errors"
)
// Service is a ReachRS service discovered via MDNS-SD
type Service struct {
Name string
AddrV4 *net.IP
Port int
}
// Discover tries to discover ReachRS services on the local network via mDNS-SD
func Discover(ctx context.Context) ([]Service, error) {
services := make([]Service, 0)
watch, err := Watch(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
for srv := range watch {
services = append(services, srv)
}
return services, nil
}
// Watch watches ReachRS services on the local network via mDNS-SD
func Watch(ctx context.Context) (chan Service, error) {
out := make(chan Service, 0)
resolver, err := zeroconf.NewResolver()
if err != nil {
return nil, errors.WithStack(err)
}
entries := make(chan *zeroconf.ServiceEntry)
go func() {
defer close(out)
for e := range entries {
var addr *net.IP
if len(e.AddrIPv4) > 0 {
addr = &e.AddrIPv4[0]
}
srv := Service{
Name: e.Instance,
AddrV4: addr,
Port: e.Port,
}
out <- srv
}
}()
if err = resolver.Browse(ctx, "_reach._tcp", ".local", entries); err != nil {
return nil, err
}
return out, nil
}

229
reach/discovery/resolver.go Normal file
View File

@ -0,0 +1,229 @@
package discovery
import (
"context"
"log"
"net"
"strings"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/mdns"
"github.com/pkg/errors"
"github.com/wlynxg/anet"
)
const ReachService = "_reach._tcp"
type Resolver struct {
}
func NewResolver() *Resolver {
r := &Resolver{}
return r
}
func (r *Resolver) Scan(ctx context.Context, interval time.Duration) (chan Service, error) {
found := make(chan Service)
entries := make(chan *mdns.ServiceEntry)
go r.listener(ctx, entries, found)
ifaces, err := findMulticastInterfaces(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
for _, iface := range ifaces {
err := func(iface net.Interface) error {
hasIPv4, hasIPv6, err := retrieveSupportedProtocols(iface)
if err != nil {
return errors.WithStack(err)
}
if !hasIPv4 && !hasIPv6 {
return nil
}
err = r.queryIface(entries, iface, !hasIPv4, !hasIPv6, interval)
if err != nil && !(errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled)) {
return errors.WithStack(err)
}
go func(iface net.Interface) {
defer close(entries)
if err := r.pollInterface(ctx, entries, iface, interval, !hasIPv4, !hasIPv6); err != nil {
log.Printf("[ERROR] %+v", errors.WithStack(err))
}
}(iface)
return nil
}(iface)
if err != nil {
return nil, errors.WithStack(err)
}
}
return found, nil
}
func (r *Resolver) queryIface(entries chan *mdns.ServiceEntry, iface net.Interface, disableIPv4, disableIPv6 bool, timeout time.Duration) error {
err := mdns.Query(&mdns.QueryParam{
Service: ReachService,
Domain: "local",
Timeout: timeout,
Entries: entries,
Interface: &iface,
DisableIPv6: disableIPv6,
DisableIPv4: disableIPv4,
})
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (r *Resolver) pollInterface(ctx context.Context, entries chan *mdns.ServiceEntry, iface net.Interface, interval time.Duration, disableIPv4, disableIPv6 bool) error {
ticker := time.NewTicker(interval)
for {
select {
case <-ticker.C:
if err := r.queryIface(entries, iface, disableIPv4, disableIPv6, interval); err != nil {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
continue
}
return errors.WithStack(err)
}
case <-ctx.Done():
if err := ctx.Err(); err != nil {
return errors.WithStack(err)
}
return nil
}
}
}
func (r *Resolver) listener(ctx context.Context, entries chan *mdns.ServiceEntry, found chan Service) {
defer close(found)
nameSeparator := "." + ReachService
for {
select {
case entry, ok := <-entries:
if !ok {
return
}
if entry == nil {
continue
}
name := strings.Split(entry.Name, nameSeparator)
if len(name) < 2 {
continue
}
info := decodeTxtRecord(entry.Info)
srv := Service{
Name: name[0],
Device: info["device"],
AddrV4: entry.AddrV4,
Port: entry.Port,
}
spew.Sdump(srv)
found <- srv
case <-ctx.Done():
return
}
}
}
func decodeTxtRecord(txt string) map[string]string {
m := make(map[string]string)
s := strings.Split(txt, "|")
for _, v := range s {
s := strings.Split(v, "=")
if len(s) == 2 {
m[s[0]] = s[1]
}
}
return m
}
func isIPv4(ip net.IP) bool {
return strings.Count(ip.String(), ":") < 2
}
func isIPv6(ip net.IP) bool {
return strings.Count(ip.String(), ":") >= 2
}
func findMulticastInterfaces(ctx context.Context) ([]net.Interface, error) {
ifaces, err := anet.Interfaces()
if err != nil {
return nil, nil
}
multicastIfaces := make([]net.Interface, 0)
for _, iface := range ifaces {
if iface.Flags&net.FlagLoopback == net.FlagLoopback {
continue
}
if iface.Flags&net.FlagRunning != net.FlagRunning {
continue
}
if iface.Flags&net.FlagMulticast != net.FlagMulticast {
continue
}
multicastIfaces = append(multicastIfaces, iface)
}
return multicastIfaces, nil
}
func retrieveSupportedProtocols(iface net.Interface) (bool, bool, error) {
adresses, err := anet.InterfaceAddrsByInterface(&iface)
if err != nil {
return false, false, errors.WithStack(err)
}
hasIPv4 := false
hasIPv6 := false
for _, addr := range adresses {
ip, _, err := net.ParseCIDR(addr.String())
if err != nil {
return false, false, errors.WithStack(err)
}
if isIPv4(ip) {
hasIPv4 = true
}
if isIPv6(ip) {
hasIPv6 = true
}
if hasIPv4 && hasIPv6 {
return hasIPv4, hasIPv6, nil
}
}
return hasIPv4, hasIPv6, nil
}

View File

@ -7,17 +7,33 @@ import (
"time"
"forge.cadoles.com/cadoles/go-emlid/reach"
"github.com/davecgh/go-spew/spew"
"github.com/pkg/errors"
)
func TestDiscovery(t *testing.T) {
func TestResolver(t *testing.T) {
reach.AssertIntegrationTests(t)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
services, err := Discover(ctx)
resolver := NewResolver()
found, err := resolver.Scan(ctx, 500*time.Millisecond)
if err != nil {
t.Fatal(err)
t.Fatalf("%+v", errors.WithStack(err))
}
services := make([]Service, 0)
OUTER:
for {
select {
case s := <-found:
t.Logf("%s", spew.Sdump(s))
services = append(services, s)
case <-ctx.Done():
break OUTER
}
}
if g, e := len(services), 1; g < e {

View File

@ -0,0 +1,10 @@
package discovery
import "net"
type Service struct {
Name string
Device string
AddrV4 net.IP
Port int
}