Integrate BACKBEAT SDK and resolve KACHING license validation

Major integrations and fixes:
- Added BACKBEAT SDK integration for P2P operation timing
- Implemented beat-aware status tracking for distributed operations
- Added Docker secrets support for secure license management
- Resolved KACHING license validation via HTTPS/TLS
- Updated docker-compose configuration for clean stack deployment
- Disabled rollback policies to prevent deployment failures
- Added license credential storage (CHORUS-DEV-MULTI-001)

Technical improvements:
- BACKBEAT P2P operation tracking with phase management
- Enhanced configuration system with file-based secrets
- Improved error handling for license validation
- Clean separation of KACHING and CHORUS deployment stacks

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-09-06 07:56:26 +10:00
parent 543ab216f9
commit 9bdcbe0447
4730 changed files with 1480093 additions and 1916 deletions

24
vendor/github.com/libp2p/zeroconf/v2/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,24 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof

27
vendor/github.com/libp2p/zeroconf/v2/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,27 @@
The MIT License (MIT)
Copyright (c) 2016 Stefan Smarzly
Copyright (c) 2014 Oleksandr Lobunets
Note: Copyright for portions of project zeroconf.sd are held by Oleksandr
Lobunets, 2014, as part of project bonjour. All other copyright for
project zeroconf.sd are held by Stefan Smarzly, 2016.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

107
vendor/github.com/libp2p/zeroconf/v2/README.md generated vendored Normal file
View File

@@ -0,0 +1,107 @@
ZeroConf: Service Discovery with mDNS
=====================================
ZeroConf is a pure Golang library that employs Multicast DNS-SD for
* browsing and resolving services in your network
* registering own services
in the local network.
It basically implements aspects of the standards
[RFC 6762](https://tools.ietf.org/html/rfc6762) (mDNS) and
[RFC 6763](https://tools.ietf.org/html/rfc6763) (DNS-SD).
Though it does not support all requirements yet, the aim is to provide a compliant solution in the long-term with the community.
By now, it should be compatible to [Avahi](http://avahi.org/) (tested) and Apple's Bonjour (untested).
Target environments: private LAN/Wifi, small or isolated networks.
[![GoDoc](https://godoc.org/github.com/libp2p/zeroconf?status.svg)](https://godoc.org/github.com/libp2p/zeroconf)
[![Go Report Card](https://goreportcard.com/badge/github.com/libp2p/zeroconf)](https://goreportcard.com/report/github.com/libp2p/zeroconf)
[![Tests](https://github.com/libp2p/zeroconf/actions/workflows/go-test.yml/badge.svg)](https://github.com/libp2p/zeroconf/actions/workflows/go-test.yml)
## Install
Nothing is as easy as that:
```bash
$ go get -u github.com/libp2p/zeroconf/v2
```
## Browse for services in your local network
```go
entries := make(chan *zeroconf.ServiceEntry)
go func(results <-chan *zeroconf.ServiceEntry) {
for entry := range results {
log.Println(entry)
}
log.Println("No more entries.")
}(entries)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
defer cancel()
// Discover all services on the network (e.g. _workstation._tcp)
err = zeroconf.Browse(ctx, "_workstation._tcp", "local.", entries)
if err != nil {
log.Fatalln("Failed to browse:", err.Error())
}
<-ctx.Done()
```
A subtype may added to service name to narrow the set of results. E.g. to browse `_workstation._tcp` with subtype `_windows`, use`_workstation._tcp,_windows`.
See https://github.com/libp2p/zeroconf/blob/master/examples/resolv/client.go.
## Lookup a specific service instance
```go
// Example filled soon.
```
## Register a service
```go
server, err := zeroconf.Register("GoZeroconf", "_workstation._tcp", "local.", 42424, []string{"txtv=0", "lo=1", "la=2"}, nil)
if err != nil {
panic(err)
}
defer server.Shutdown()
// Clean exit.
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
select {
case <-sig:
// Exit by user
case <-time.After(time.Second * 120):
// Exit by timeout
}
log.Println("Shutting down.")
```
Multiple subtypes may be added to service name, separated by commas. E.g `_workstation._tcp,_windows` has subtype `_windows`.
See https://github.com/libp2p/zeroconf/blob/master/examples/register/server.go.
## Features and ToDo's
This list gives a quick impression about the state of this library.
See what needs to be done and submit a pull request :)
* [x] Browse / Lookup / Register services
* [x] Multiple IPv6 / IPv4 addresses support
* [x] Send multiple probes (exp. back-off) if no service answers (*)
* [x] Timestamp entries for TTL checks
* [ ] Compare new multicasts with already received services
_Notes:_
(*) The denoted features might not be perfectly standards compliant, but shouldn't cause any problems.
Some tests showed improvements in overall robustness and performance with the features enabled.
## Credits
Great thanks to [hashicorp](https://github.com/hashicorp/mdns) and to [oleksandr](https://github.com/oleksandr/bonjour) and all contributing authors for the code this projects bases upon.
Large parts of the code are still the same.
However, there are several reasons why I decided to create a fork of the original project:
The previous project seems to be unmaintained. There are several useful pull requests waiting. I merged most of them in this project.
Still, the implementation has some bugs and lacks some other features that make it quite unreliable in real LAN environments when running continously.
Last but not least, the aim for this project is to build a solution that targets standard conformance in the long term with the support of the community.
Though, resiliency should remain a top goal.

475
vendor/github.com/libp2p/zeroconf/v2/client.go generated vendored Normal file
View File

@@ -0,0 +1,475 @@
package zeroconf
import (
"context"
"fmt"
"log"
"math/rand"
"net"
"runtime"
"strings"
"time"
"github.com/miekg/dns"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)
// IPType specifies the IP traffic the client listens for.
// This does not guarantee that only mDNS entries of this sepcific
// type passes. E.g. typical mDNS packets distributed via IPv4, often contain
// both DNS A and AAAA entries.
type IPType uint8
// Options for IPType.
const (
IPv4 IPType = 0x01
IPv6 IPType = 0x02
IPv4AndIPv6 = IPv4 | IPv6 // default option
)
var initialQueryInterval = 4 * time.Second
// Client structure encapsulates both IPv4/IPv6 UDP connections.
type client struct {
ipv4conn *ipv4.PacketConn
ipv6conn *ipv6.PacketConn
ifaces []net.Interface
}
type clientOpts struct {
listenOn IPType
ifaces []net.Interface
}
// ClientOption fills the option struct to configure intefaces, etc.
type ClientOption func(*clientOpts)
// SelectIPTraffic selects the type of IP packets (IPv4, IPv6, or both) this
// instance listens for.
// This does not guarantee that only mDNS entries of this sepcific
// type passes. E.g. typical mDNS packets distributed via IPv4, may contain
// both DNS A and AAAA entries.
func SelectIPTraffic(t IPType) ClientOption {
return func(o *clientOpts) {
o.listenOn = t
}
}
// SelectIfaces selects the interfaces to query for mDNS records
func SelectIfaces(ifaces []net.Interface) ClientOption {
return func(o *clientOpts) {
o.ifaces = ifaces
}
}
// Browse for all services of a given type in a given domain.
// Received entries are sent on the entries channel.
// It blocks until the context is canceled (or an error occurs).
func Browse(ctx context.Context, service, domain string, entries chan<- *ServiceEntry, opts ...ClientOption) error {
cl, err := newClient(applyOpts(opts...))
if err != nil {
return err
}
params := defaultParams(service)
if domain != "" {
params.Domain = domain
}
params.Entries = entries
params.isBrowsing = true
return cl.run(ctx, params)
}
// Lookup a specific service by its name and type in a given domain.
// Received entries are sent on the entries channel.
// It blocks until the context is canceled (or an error occurs).
func Lookup(ctx context.Context, instance, service, domain string, entries chan<- *ServiceEntry, opts ...ClientOption) error {
cl, err := newClient(applyOpts(opts...))
if err != nil {
return err
}
params := defaultParams(service)
params.Instance = instance
if domain != "" {
params.Domain = domain
}
params.Entries = entries
return cl.run(ctx, params)
}
func applyOpts(options ...ClientOption) clientOpts {
// Apply default configuration and load supplied options.
var conf = clientOpts{
listenOn: IPv4AndIPv6,
}
for _, o := range options {
if o != nil {
o(&conf)
}
}
return conf
}
func (c *client) run(ctx context.Context, params *lookupParams) error {
ctx, cancel := context.WithCancel(ctx)
done := make(chan struct{})
go func() {
defer close(done)
c.mainloop(ctx, params)
}()
// If previous probe was ok, it should be fine now. In case of an error later on,
// the entries' queue is closed.
err := c.periodicQuery(ctx, params)
cancel()
<-done
return err
}
// defaultParams returns a default set of QueryParams.
func defaultParams(service string) *lookupParams {
return newLookupParams("", service, "local", false, make(chan *ServiceEntry))
}
// Client structure constructor
func newClient(opts clientOpts) (*client, error) {
ifaces := opts.ifaces
if len(ifaces) == 0 {
ifaces = listMulticastInterfaces()
}
// IPv4 interfaces
var ipv4conn *ipv4.PacketConn
if (opts.listenOn & IPv4) > 0 {
var err error
ipv4conn, err = joinUdp4Multicast(ifaces)
if err != nil {
return nil, err
}
}
// IPv6 interfaces
var ipv6conn *ipv6.PacketConn
if (opts.listenOn & IPv6) > 0 {
var err error
ipv6conn, err = joinUdp6Multicast(ifaces)
if err != nil {
return nil, err
}
}
return &client{
ipv4conn: ipv4conn,
ipv6conn: ipv6conn,
ifaces: ifaces,
}, nil
}
var cleanupFreq = 10 * time.Second
// Start listeners and waits for the shutdown signal from exit channel
func (c *client) mainloop(ctx context.Context, params *lookupParams) {
// start listening for responses
msgCh := make(chan *dns.Msg, 32)
if c.ipv4conn != nil {
go c.recv(ctx, c.ipv4conn, msgCh)
}
if c.ipv6conn != nil {
go c.recv(ctx, c.ipv6conn, msgCh)
}
// Iterate through channels from listeners goroutines
var entries map[string]*ServiceEntry
sentEntries := make(map[string]*ServiceEntry)
ticker := time.NewTicker(cleanupFreq)
defer ticker.Stop()
for {
var now time.Time
select {
case <-ctx.Done():
// Context expired. Notify subscriber that we are done here.
params.done()
c.shutdown()
return
case t := <-ticker.C:
for k, e := range sentEntries {
if t.After(e.Expiry) {
delete(sentEntries, k)
}
}
continue
case msg := <-msgCh:
now = time.Now()
entries = make(map[string]*ServiceEntry)
sections := append(msg.Answer, msg.Ns...)
sections = append(sections, msg.Extra...)
for _, answer := range sections {
switch rr := answer.(type) {
case *dns.PTR:
if params.ServiceName() != rr.Hdr.Name {
continue
}
if params.ServiceInstanceName() != "" && params.ServiceInstanceName() != rr.Ptr {
continue
}
if _, ok := entries[rr.Ptr]; !ok {
entries[rr.Ptr] = newServiceEntry(
trimDot(strings.Replace(rr.Ptr, rr.Hdr.Name, "", -1)),
params.Service,
params.Domain)
}
entries[rr.Ptr].Expiry = now.Add(time.Duration(rr.Hdr.Ttl) * time.Second)
case *dns.SRV:
if params.ServiceInstanceName() != "" && params.ServiceInstanceName() != rr.Hdr.Name {
continue
} else if !strings.HasSuffix(rr.Hdr.Name, params.ServiceName()) {
continue
}
if _, ok := entries[rr.Hdr.Name]; !ok {
entries[rr.Hdr.Name] = newServiceEntry(
trimDot(strings.Replace(rr.Hdr.Name, params.ServiceName(), "", 1)),
params.Service,
params.Domain)
}
entries[rr.Hdr.Name].HostName = rr.Target
entries[rr.Hdr.Name].Port = int(rr.Port)
entries[rr.Hdr.Name].Expiry = now.Add(time.Duration(rr.Hdr.Ttl) * time.Second)
case *dns.TXT:
if params.ServiceInstanceName() != "" && params.ServiceInstanceName() != rr.Hdr.Name {
continue
} else if !strings.HasSuffix(rr.Hdr.Name, params.ServiceName()) {
continue
}
if _, ok := entries[rr.Hdr.Name]; !ok {
entries[rr.Hdr.Name] = newServiceEntry(
trimDot(strings.Replace(rr.Hdr.Name, params.ServiceName(), "", 1)),
params.Service,
params.Domain)
}
entries[rr.Hdr.Name].Text = rr.Txt
entries[rr.Hdr.Name].Expiry = now.Add(time.Duration(rr.Hdr.Ttl) * time.Second)
}
}
// Associate IPs in a second round as other fields should be filled by now.
for _, answer := range sections {
switch rr := answer.(type) {
case *dns.A:
for k, e := range entries {
if e.HostName == rr.Hdr.Name {
entries[k].AddrIPv4 = append(entries[k].AddrIPv4, rr.A)
}
}
case *dns.AAAA:
for k, e := range entries {
if e.HostName == rr.Hdr.Name {
entries[k].AddrIPv6 = append(entries[k].AddrIPv6, rr.AAAA)
}
}
}
}
}
if len(entries) > 0 {
for k, e := range entries {
if !e.Expiry.After(now) {
delete(entries, k)
delete(sentEntries, k)
continue
}
if _, ok := sentEntries[k]; ok {
continue
}
// If this is an DNS-SD query do not throw PTR away.
// It is expected to have only PTR for enumeration
if params.ServiceRecord.ServiceTypeName() != params.ServiceRecord.ServiceName() {
// Require at least one resolved IP address for ServiceEntry
// TODO: wait some more time as chances are high both will arrive.
if len(e.AddrIPv4) == 0 && len(e.AddrIPv6) == 0 {
continue
}
}
// Submit entry to subscriber and cache it.
// This is also a point to possibly stop probing actively for a
// service entry.
params.Entries <- e
sentEntries[k] = e
if !params.isBrowsing {
params.disableProbing()
}
}
}
}
}
// Shutdown client will close currently open connections and channel implicitly.
func (c *client) shutdown() {
if c.ipv4conn != nil {
c.ipv4conn.Close()
}
if c.ipv6conn != nil {
c.ipv6conn.Close()
}
}
// Data receiving routine reads from connection, unpacks packets into dns.Msg
// structures and sends them to a given msgCh channel
func (c *client) recv(ctx context.Context, l interface{}, msgCh chan *dns.Msg) {
var readFrom func([]byte) (n int, src net.Addr, err error)
switch pConn := l.(type) {
case *ipv6.PacketConn:
readFrom = func(b []byte) (n int, src net.Addr, err error) {
n, _, src, err = pConn.ReadFrom(b)
return
}
case *ipv4.PacketConn:
readFrom = func(b []byte) (n int, src net.Addr, err error) {
n, _, src, err = pConn.ReadFrom(b)
return
}
default:
return
}
buf := make([]byte, 65536)
var fatalErr error
for {
// Handles the following cases:
// - ReadFrom aborts with error due to closed UDP connection -> causes ctx cancel
// - ReadFrom aborts otherwise.
// TODO: the context check can be removed. Verify!
if ctx.Err() != nil || fatalErr != nil {
return
}
n, _, err := readFrom(buf)
if err != nil {
fatalErr = err
continue
}
msg := new(dns.Msg)
if err := msg.Unpack(buf[:n]); err != nil {
// log.Printf("[WARN] mdns: Failed to unpack packet: %v", err)
continue
}
select {
case msgCh <- msg:
// Submit decoded DNS message and continue.
case <-ctx.Done():
// Abort.
return
}
}
}
// periodicQuery sens multiple probes until a valid response is received by
// the main processing loop or some timeout/cancel fires.
// TODO: move error reporting to shutdown function as periodicQuery is called from
// go routine context.
func (c *client) periodicQuery(ctx context.Context, params *lookupParams) error {
// Do the first query immediately.
if err := c.query(params); err != nil {
return err
}
const maxInterval = 60 * time.Second
interval := initialQueryInterval
timer := time.NewTimer(interval)
defer timer.Stop()
for {
select {
case <-timer.C:
// Wait for next iteration.
case <-params.stopProbing:
// Chan is closed (or happened in the past).
// Done here. Received a matching mDNS entry.
return nil
case <-ctx.Done():
if params.isBrowsing {
return nil
}
return ctx.Err()
}
if err := c.query(params); err != nil {
return err
}
// Exponential increase of the interval with jitter:
// the new interval will be between 1.5x and 2.5x the old interval, capped at maxInterval.
if interval != maxInterval {
interval += time.Duration(rand.Int63n(interval.Nanoseconds())) + interval/2
if interval > maxInterval {
interval = maxInterval
}
}
timer.Reset(interval)
}
}
// Performs the actual query by service name (browse) or service instance name (lookup),
// start response listeners goroutines and loops over the entries channel.
func (c *client) query(params *lookupParams) error {
var serviceName, serviceInstanceName string
serviceName = fmt.Sprintf("%s.%s.", trimDot(params.Service), trimDot(params.Domain))
// send the query
m := new(dns.Msg)
if params.Instance != "" { // service instance name lookup
serviceInstanceName = fmt.Sprintf("%s.%s", params.Instance, serviceName)
m.Question = []dns.Question{
{Name: serviceInstanceName, Qtype: dns.TypeSRV, Qclass: dns.ClassINET},
{Name: serviceInstanceName, Qtype: dns.TypeTXT, Qclass: dns.ClassINET},
}
} else if len(params.Subtypes) > 0 { // service subtype browse
m.SetQuestion(params.Subtypes[0], dns.TypePTR)
} else { // service name browse
m.SetQuestion(serviceName, dns.TypePTR)
}
m.RecursionDesired = false
return c.sendQuery(m)
}
// Pack the dns.Msg and write to available connections (multicast)
func (c *client) sendQuery(msg *dns.Msg) error {
buf, err := msg.Pack()
if err != nil {
return err
}
if c.ipv4conn != nil {
// See https://pkg.go.dev/golang.org/x/net/ipv4#pkg-note-BUG
// As of Golang 1.18.4
// On Windows, the ControlMessage for ReadFrom and WriteTo methods of PacketConn is not implemented.
var wcm ipv4.ControlMessage
for ifi := range c.ifaces {
switch runtime.GOOS {
case "darwin", "ios", "linux":
wcm.IfIndex = c.ifaces[ifi].Index
default:
if err := c.ipv4conn.SetMulticastInterface(&c.ifaces[ifi]); err != nil {
log.Printf("[WARN] mdns: Failed to set multicast interface: %v", err)
}
}
c.ipv4conn.WriteTo(buf, &wcm, ipv4Addr)
}
}
if c.ipv6conn != nil {
// See https://pkg.go.dev/golang.org/x/net/ipv6#pkg-note-BUG
// As of Golang 1.18.4
// On Windows, the ControlMessage for ReadFrom and WriteTo methods of PacketConn is not implemented.
var wcm ipv6.ControlMessage
for ifi := range c.ifaces {
switch runtime.GOOS {
case "darwin", "ios", "linux":
wcm.IfIndex = c.ifaces[ifi].Index
default:
if err := c.ipv6conn.SetMulticastInterface(&c.ifaces[ifi]); err != nil {
log.Printf("[WARN] mdns: Failed to set multicast interface: %v", err)
}
}
c.ipv6conn.WriteTo(buf, &wcm, ipv6Addr)
}
}
return nil
}

119
vendor/github.com/libp2p/zeroconf/v2/connection.go generated vendored Normal file
View File

@@ -0,0 +1,119 @@
package zeroconf
import (
"fmt"
"net"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)
var (
// Multicast groups used by mDNS
mdnsGroupIPv4 = net.IPv4(224, 0, 0, 251)
mdnsGroupIPv6 = net.ParseIP("ff02::fb")
// mDNS wildcard addresses
mdnsWildcardAddrIPv4 = &net.UDPAddr{
IP: net.ParseIP("224.0.0.0"),
Port: 5353,
}
mdnsWildcardAddrIPv6 = &net.UDPAddr{
IP: net.ParseIP("ff02::"),
// IP: net.ParseIP("fd00::12d3:26e7:48db:e7d"),
Port: 5353,
}
// mDNS endpoint addresses
ipv4Addr = &net.UDPAddr{
IP: mdnsGroupIPv4,
Port: 5353,
}
ipv6Addr = &net.UDPAddr{
IP: mdnsGroupIPv6,
Port: 5353,
}
)
func joinUdp6Multicast(interfaces []net.Interface) (*ipv6.PacketConn, error) {
udpConn, err := net.ListenUDP("udp6", mdnsWildcardAddrIPv6)
if err != nil {
return nil, err
}
// Join multicast groups to receive announcements
pkConn := ipv6.NewPacketConn(udpConn)
pkConn.SetControlMessage(ipv6.FlagInterface, true)
if len(interfaces) == 0 {
interfaces = listMulticastInterfaces()
}
// log.Println("Using multicast interfaces: ", interfaces)
var failedJoins int
for _, iface := range interfaces {
if err := pkConn.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil {
// log.Println("Udp6 JoinGroup failed for iface ", iface)
failedJoins++
}
}
if failedJoins == len(interfaces) {
pkConn.Close()
return nil, fmt.Errorf("udp6: failed to join any of these interfaces: %v", interfaces)
}
_ = pkConn.SetMulticastHopLimit(255)
return pkConn, nil
}
func joinUdp4Multicast(interfaces []net.Interface) (*ipv4.PacketConn, error) {
udpConn, err := net.ListenUDP("udp4", mdnsWildcardAddrIPv4)
if err != nil {
// log.Printf("[ERR] bonjour: Failed to bind to udp4 mutlicast: %v", err)
return nil, err
}
// Join multicast groups to receive announcements
pkConn := ipv4.NewPacketConn(udpConn)
pkConn.SetControlMessage(ipv4.FlagInterface, true)
if len(interfaces) == 0 {
interfaces = listMulticastInterfaces()
}
// log.Println("Using multicast interfaces: ", interfaces)
var failedJoins int
for _, iface := range interfaces {
if err := pkConn.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil {
// log.Println("Udp4 JoinGroup failed for iface ", iface)
failedJoins++
}
}
if failedJoins == len(interfaces) {
pkConn.Close()
return nil, fmt.Errorf("udp4: failed to join any of these interfaces: %v", interfaces)
}
_ = pkConn.SetMulticastTTL(255)
return pkConn, nil
}
func listMulticastInterfaces() []net.Interface {
var interfaces []net.Interface
ifaces, err := net.Interfaces()
if err != nil {
return nil
}
for _, ifi := range ifaces {
if (ifi.Flags & net.FlagUp) == 0 {
continue
}
if (ifi.Flags & net.FlagMulticast) > 0 {
interfaces = append(interfaces, ifi)
}
}
return interfaces
}

14
vendor/github.com/libp2p/zeroconf/v2/doc.go generated vendored Normal file
View File

@@ -0,0 +1,14 @@
// Package zeroconf is a pure Golang library that employs Multicast DNS-SD for
// browsing and resolving services in your network and registering own services
// in the local network.
//
// It basically implements aspects of the standards
// RFC 6762 (mDNS) and
// RFC 6763 (DNS-SD).
// Though it does not support all requirements yet, the aim is to provide a
// complient solution in the long-term with the community.
//
// By now, it should be compatible to [Avahi](http://avahi.org/) (tested) and
// Apple's Bonjour (untested). Should work in the most office, home and private
// environments.
package zeroconf

839
vendor/github.com/libp2p/zeroconf/v2/server.go generated vendored Normal file
View File

@@ -0,0 +1,839 @@
package zeroconf
import (
"fmt"
"log"
"math/rand"
"net"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/miekg/dns"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)
const (
// Number of Multicast responses sent for a query message (default: 1 < x < 9)
multicastRepetitions = 2
)
var defaultTTL uint32 = 3200
type serverOpts struct {
ttl uint32
}
func applyServerOpts(options ...ServerOption) serverOpts {
// Apply default configuration and load supplied options.
var conf = serverOpts{
ttl: defaultTTL,
}
for _, o := range options {
if o != nil {
o(&conf)
}
}
return conf
}
// ServerOption fills the option struct.
type ServerOption func(*serverOpts)
// TTL sets the TTL for DNS replies.
func TTL(ttl uint32) ServerOption {
return func(o *serverOpts) {
o.ttl = ttl
}
}
// Register a service by given arguments. This call will take the system's hostname
// and lookup IP by that hostname.
func Register(instance, service, domain string, port int, text []string, ifaces []net.Interface, opts ...ServerOption) (*Server, error) {
entry := newServiceEntry(instance, service, domain)
entry.Port = port
entry.Text = text
if entry.Instance == "" {
return nil, fmt.Errorf("missing service instance name")
}
if entry.Service == "" {
return nil, fmt.Errorf("missing service name")
}
if entry.Domain == "" {
entry.Domain = "local."
}
if entry.Port == 0 {
return nil, fmt.Errorf("missing port")
}
var err error
if entry.HostName == "" {
entry.HostName, err = os.Hostname()
if err != nil {
return nil, fmt.Errorf("could not determine host")
}
}
if !strings.HasSuffix(trimDot(entry.HostName), entry.Domain) {
entry.HostName = fmt.Sprintf("%s.%s.", trimDot(entry.HostName), trimDot(entry.Domain))
}
if len(ifaces) == 0 {
ifaces = listMulticastInterfaces()
}
for _, iface := range ifaces {
v4, v6 := addrsForInterface(&iface)
entry.AddrIPv4 = append(entry.AddrIPv4, v4...)
entry.AddrIPv6 = append(entry.AddrIPv6, v6...)
}
if entry.AddrIPv4 == nil && entry.AddrIPv6 == nil {
return nil, fmt.Errorf("could not determine host IP addresses")
}
s, err := newServer(ifaces, applyServerOpts(opts...))
if err != nil {
return nil, err
}
s.service = entry
s.start()
return s, nil
}
// RegisterProxy registers a service proxy. This call will skip the hostname/IP lookup and
// will use the provided values.
func RegisterProxy(instance, service, domain string, port int, host string, ips []string, text []string, ifaces []net.Interface, opts ...ServerOption) (*Server, error) {
entry := newServiceEntry(instance, service, domain)
entry.Port = port
entry.Text = text
entry.HostName = host
if entry.Instance == "" {
return nil, fmt.Errorf("missing service instance name")
}
if entry.Service == "" {
return nil, fmt.Errorf("missing service name")
}
if entry.HostName == "" {
return nil, fmt.Errorf("missing host name")
}
if entry.Domain == "" {
entry.Domain = "local"
}
if entry.Port == 0 {
return nil, fmt.Errorf("missing port")
}
if !strings.HasSuffix(trimDot(entry.HostName), entry.Domain) {
entry.HostName = fmt.Sprintf("%s.%s.", trimDot(entry.HostName), trimDot(entry.Domain))
}
for _, ip := range ips {
ipAddr := net.ParseIP(ip)
if ipAddr == nil {
return nil, fmt.Errorf("failed to parse given IP: %v", ip)
} else if ipv4 := ipAddr.To4(); ipv4 != nil {
entry.AddrIPv4 = append(entry.AddrIPv4, ipAddr)
} else if ipv6 := ipAddr.To16(); ipv6 != nil {
entry.AddrIPv6 = append(entry.AddrIPv6, ipAddr)
} else {
return nil, fmt.Errorf("the IP is neither IPv4 nor IPv6: %#v", ipAddr)
}
}
if len(ifaces) == 0 {
ifaces = listMulticastInterfaces()
}
s, err := newServer(ifaces, applyServerOpts(opts...))
if err != nil {
return nil, err
}
s.service = entry
s.start()
return s, nil
}
const (
qClassCacheFlush uint16 = 1 << 15
)
// Server structure encapsulates both IPv4/IPv6 UDP connections
type Server struct {
service *ServiceEntry
ipv4conn *ipv4.PacketConn
ipv6conn *ipv6.PacketConn
ifaces []net.Interface
shouldShutdown chan struct{}
shutdownLock sync.Mutex
refCount sync.WaitGroup
isShutdown bool
ttl uint32
}
// Constructs server structure
func newServer(ifaces []net.Interface, opts serverOpts) (*Server, error) {
ipv4conn, err4 := joinUdp4Multicast(ifaces)
if err4 != nil {
log.Printf("[zeroconf] no suitable IPv4 interface: %s", err4.Error())
}
ipv6conn, err6 := joinUdp6Multicast(ifaces)
if err6 != nil {
log.Printf("[zeroconf] no suitable IPv6 interface: %s", err6.Error())
}
if err4 != nil && err6 != nil {
// No supported interface left.
return nil, fmt.Errorf("no supported interface")
}
s := &Server{
ipv4conn: ipv4conn,
ipv6conn: ipv6conn,
ifaces: ifaces,
ttl: opts.ttl,
shouldShutdown: make(chan struct{}),
}
return s, nil
}
func (s *Server) start() {
if s.ipv4conn != nil {
s.refCount.Add(1)
go s.recv4(s.ipv4conn)
}
if s.ipv6conn != nil {
s.refCount.Add(1)
go s.recv6(s.ipv6conn)
}
s.refCount.Add(1)
go s.probe()
}
// SetText updates and announces the TXT records
func (s *Server) SetText(text []string) {
s.service.Text = text
s.announceText()
}
// TTL sets the TTL for DNS replies
//
// Deprecated: This method is racy. Use the TTL server option instead.
func (s *Server) TTL(ttl uint32) {
s.ttl = ttl
}
// Shutdown closes all udp connections and unregisters the service
func (s *Server) Shutdown() {
s.shutdownLock.Lock()
defer s.shutdownLock.Unlock()
if s.isShutdown {
return
}
if err := s.unregister(); err != nil {
log.Printf("failed to unregister: %s", err)
}
close(s.shouldShutdown)
if s.ipv4conn != nil {
s.ipv4conn.Close()
}
if s.ipv6conn != nil {
s.ipv6conn.Close()
}
// Wait for connection and routines to be closed
s.refCount.Wait()
s.isShutdown = true
}
// recv4 is a long running routine to receive packets from an interface
func (s *Server) recv4(c *ipv4.PacketConn) {
defer s.refCount.Done()
if c == nil {
return
}
buf := make([]byte, 65536)
for {
select {
case <-s.shouldShutdown:
return
default:
var ifIndex int
n, cm, from, err := c.ReadFrom(buf)
if err != nil {
continue
}
if cm != nil {
ifIndex = cm.IfIndex
}
_ = s.parsePacket(buf[:n], ifIndex, from)
}
}
}
// recv6 is a long running routine to receive packets from an interface
func (s *Server) recv6(c *ipv6.PacketConn) {
defer s.refCount.Done()
if c == nil {
return
}
buf := make([]byte, 65536)
for {
select {
case <-s.shouldShutdown:
return
default:
var ifIndex int
n, cm, from, err := c.ReadFrom(buf)
if err != nil {
continue
}
if cm != nil {
ifIndex = cm.IfIndex
}
_ = s.parsePacket(buf[:n], ifIndex, from)
}
}
}
// parsePacket is used to parse an incoming packet
func (s *Server) parsePacket(packet []byte, ifIndex int, from net.Addr) error {
var msg dns.Msg
if err := msg.Unpack(packet); err != nil {
// log.Printf("[ERR] zeroconf: Failed to unpack packet: %v", err)
return err
}
return s.handleQuery(&msg, ifIndex, from)
}
// handleQuery is used to handle an incoming query
func (s *Server) handleQuery(query *dns.Msg, ifIndex int, from net.Addr) error {
// Ignore questions with authoritative section for now
if len(query.Ns) > 0 {
return nil
}
// Handle each question
var err error
for _, q := range query.Question {
resp := dns.Msg{}
resp.SetReply(query)
resp.Compress = true
resp.RecursionDesired = false
resp.Authoritative = true
resp.Question = nil // RFC6762 section 6 "responses MUST NOT contain any questions"
resp.Answer = []dns.RR{}
resp.Extra = []dns.RR{}
if err = s.handleQuestion(q, &resp, query, ifIndex); err != nil {
// log.Printf("[ERR] zeroconf: failed to handle question %v: %v", q, err)
continue
}
// Check if there is an answer
if len(resp.Answer) == 0 {
continue
}
if isUnicastQuestion(q) {
// Send unicast
if e := s.unicastResponse(&resp, ifIndex, from); e != nil {
err = e
}
} else {
// Send mulicast
if e := s.multicastResponse(&resp, ifIndex); e != nil {
err = e
}
}
}
return err
}
// RFC6762 7.1. Known-Answer Suppression
func isKnownAnswer(resp *dns.Msg, query *dns.Msg) bool {
if len(resp.Answer) == 0 || len(query.Answer) == 0 {
return false
}
if resp.Answer[0].Header().Rrtype != dns.TypePTR {
return false
}
answer := resp.Answer[0].(*dns.PTR)
for _, known := range query.Answer {
hdr := known.Header()
if hdr.Rrtype != answer.Hdr.Rrtype {
continue
}
ptr := known.(*dns.PTR)
if ptr.Ptr == answer.Ptr && hdr.Ttl >= answer.Hdr.Ttl/2 {
// log.Printf("skipping known answer: %v", ptr)
return true
}
}
return false
}
// handleQuestion is used to handle an incoming question
func (s *Server) handleQuestion(q dns.Question, resp *dns.Msg, query *dns.Msg, ifIndex int) error {
if s.service == nil {
return nil
}
switch q.Name {
case s.service.ServiceTypeName():
s.serviceTypeName(resp, s.ttl)
if isKnownAnswer(resp, query) {
resp.Answer = nil
}
case s.service.ServiceName():
s.composeBrowsingAnswers(resp, ifIndex)
if isKnownAnswer(resp, query) {
resp.Answer = nil
}
case s.service.ServiceInstanceName():
s.composeLookupAnswers(resp, s.ttl, ifIndex, false)
default:
// handle matching subtype query
for _, subtype := range s.service.Subtypes {
subtype = fmt.Sprintf("%s._sub.%s", subtype, s.service.ServiceName())
if q.Name == subtype {
s.composeBrowsingAnswers(resp, ifIndex)
if isKnownAnswer(resp, query) {
resp.Answer = nil
}
break
}
}
}
return nil
}
func (s *Server) composeBrowsingAnswers(resp *dns.Msg, ifIndex int) {
ptr := &dns.PTR{
Hdr: dns.RR_Header{
Name: s.service.ServiceName(),
Rrtype: dns.TypePTR,
Class: dns.ClassINET,
Ttl: s.ttl,
},
Ptr: s.service.ServiceInstanceName(),
}
resp.Answer = append(resp.Answer, ptr)
txt := &dns.TXT{
Hdr: dns.RR_Header{
Name: s.service.ServiceInstanceName(),
Rrtype: dns.TypeTXT,
Class: dns.ClassINET,
Ttl: s.ttl,
},
Txt: s.service.Text,
}
srv := &dns.SRV{
Hdr: dns.RR_Header{
Name: s.service.ServiceInstanceName(),
Rrtype: dns.TypeSRV,
Class: dns.ClassINET,
Ttl: s.ttl,
},
Priority: 0,
Weight: 0,
Port: uint16(s.service.Port),
Target: s.service.HostName,
}
resp.Extra = append(resp.Extra, srv, txt)
resp.Extra = s.appendAddrs(resp.Extra, s.ttl, ifIndex, false)
}
func (s *Server) composeLookupAnswers(resp *dns.Msg, ttl uint32, ifIndex int, flushCache bool) {
// From RFC6762
// The most significant bit of the rrclass for a record in the Answer
// Section of a response message is the Multicast DNS cache-flush bit
// and is discussed in more detail below in Section 10.2, "Announcements
// to Flush Outdated Cache Entries".
ptr := &dns.PTR{
Hdr: dns.RR_Header{
Name: s.service.ServiceName(),
Rrtype: dns.TypePTR,
Class: dns.ClassINET,
Ttl: ttl,
},
Ptr: s.service.ServiceInstanceName(),
}
srv := &dns.SRV{
Hdr: dns.RR_Header{
Name: s.service.ServiceInstanceName(),
Rrtype: dns.TypeSRV,
Class: dns.ClassINET | qClassCacheFlush,
Ttl: ttl,
},
Priority: 0,
Weight: 0,
Port: uint16(s.service.Port),
Target: s.service.HostName,
}
txt := &dns.TXT{
Hdr: dns.RR_Header{
Name: s.service.ServiceInstanceName(),
Rrtype: dns.TypeTXT,
Class: dns.ClassINET | qClassCacheFlush,
Ttl: ttl,
},
Txt: s.service.Text,
}
dnssd := &dns.PTR{
Hdr: dns.RR_Header{
Name: s.service.ServiceTypeName(),
Rrtype: dns.TypePTR,
Class: dns.ClassINET,
Ttl: ttl,
},
Ptr: s.service.ServiceName(),
}
resp.Answer = append(resp.Answer, srv, txt, ptr, dnssd)
for _, subtype := range s.service.Subtypes {
resp.Answer = append(resp.Answer,
&dns.PTR{
Hdr: dns.RR_Header{
Name: subtype,
Rrtype: dns.TypePTR,
Class: dns.ClassINET,
Ttl: ttl,
},
Ptr: s.service.ServiceInstanceName(),
})
}
resp.Answer = s.appendAddrs(resp.Answer, ttl, ifIndex, flushCache)
}
func (s *Server) serviceTypeName(resp *dns.Msg, ttl uint32) {
// From RFC6762
// 9. Service Type Enumeration
//
// For this purpose, a special meta-query is defined. A DNS query for
// PTR records with the name "_services._dns-sd._udp.<Domain>" yields a
// set of PTR records, where the rdata of each PTR record is the two-
// label <Service> name, plus the same domain, e.g.,
// "_http._tcp.<Domain>".
dnssd := &dns.PTR{
Hdr: dns.RR_Header{
Name: s.service.ServiceTypeName(),
Rrtype: dns.TypePTR,
Class: dns.ClassINET,
Ttl: ttl,
},
Ptr: s.service.ServiceName(),
}
resp.Answer = append(resp.Answer, dnssd)
}
// Perform probing & announcement
//TODO: implement a proper probing & conflict resolution
func (s *Server) probe() {
defer s.refCount.Done()
q := new(dns.Msg)
q.SetQuestion(s.service.ServiceInstanceName(), dns.TypePTR)
q.RecursionDesired = false
srv := &dns.SRV{
Hdr: dns.RR_Header{
Name: s.service.ServiceInstanceName(),
Rrtype: dns.TypeSRV,
Class: dns.ClassINET,
Ttl: s.ttl,
},
Priority: 0,
Weight: 0,
Port: uint16(s.service.Port),
Target: s.service.HostName,
}
txt := &dns.TXT{
Hdr: dns.RR_Header{
Name: s.service.ServiceInstanceName(),
Rrtype: dns.TypeTXT,
Class: dns.ClassINET,
Ttl: s.ttl,
},
Txt: s.service.Text,
}
q.Ns = []dns.RR{srv, txt}
// Wait for a random duration uniformly distributed between 0 and 250 ms
// before sending the first probe packet.
timer := time.NewTimer(time.Duration(rand.Intn(250)) * time.Millisecond)
defer timer.Stop()
select {
case <-timer.C:
case <-s.shouldShutdown:
return
}
for i := 0; i < 3; i++ {
if err := s.multicastResponse(q, 0); err != nil {
log.Println("[ERR] zeroconf: failed to send probe:", err.Error())
}
timer.Reset(250 * time.Millisecond)
select {
case <-timer.C:
case <-s.shouldShutdown:
return
}
}
// From RFC6762
// The Multicast DNS responder MUST send at least two unsolicited
// responses, one second apart. To provide increased robustness against
// packet loss, a responder MAY send up to eight unsolicited responses,
// provided that the interval between unsolicited responses increases by
// at least a factor of two with every response sent.
timeout := time.Second
for i := 0; i < multicastRepetitions; i++ {
for _, intf := range s.ifaces {
resp := new(dns.Msg)
resp.MsgHdr.Response = true
// TODO: make response authoritative if we are the publisher
resp.Compress = true
resp.Answer = []dns.RR{}
resp.Extra = []dns.RR{}
s.composeLookupAnswers(resp, s.ttl, intf.Index, true)
if err := s.multicastResponse(resp, intf.Index); err != nil {
log.Println("[ERR] zeroconf: failed to send announcement:", err.Error())
}
}
timer.Reset(timeout)
select {
case <-timer.C:
case <-s.shouldShutdown:
return
}
timeout *= 2
}
}
// announceText sends a Text announcement with cache flush enabled
func (s *Server) announceText() {
resp := new(dns.Msg)
resp.MsgHdr.Response = true
txt := &dns.TXT{
Hdr: dns.RR_Header{
Name: s.service.ServiceInstanceName(),
Rrtype: dns.TypeTXT,
Class: dns.ClassINET | qClassCacheFlush,
Ttl: s.ttl,
},
Txt: s.service.Text,
}
resp.Answer = []dns.RR{txt}
s.multicastResponse(resp, 0)
}
func (s *Server) unregister() error {
resp := new(dns.Msg)
resp.MsgHdr.Response = true
resp.Answer = []dns.RR{}
resp.Extra = []dns.RR{}
s.composeLookupAnswers(resp, 0, 0, true)
return s.multicastResponse(resp, 0)
}
func (s *Server) appendAddrs(list []dns.RR, ttl uint32, ifIndex int, flushCache bool) []dns.RR {
v4 := s.service.AddrIPv4
v6 := s.service.AddrIPv6
if len(v4) == 0 && len(v6) == 0 {
iface, _ := net.InterfaceByIndex(ifIndex)
if iface != nil {
a4, a6 := addrsForInterface(iface)
v4 = append(v4, a4...)
v6 = append(v6, a6...)
}
}
if ttl > 0 {
// RFC6762 Section 10 says A/AAAA records SHOULD
// use TTL of 120s, to account for network interface
// and IP address changes.
ttl = 120
}
var cacheFlushBit uint16
if flushCache {
cacheFlushBit = qClassCacheFlush
}
for _, ipv4 := range v4 {
a := &dns.A{
Hdr: dns.RR_Header{
Name: s.service.HostName,
Rrtype: dns.TypeA,
Class: dns.ClassINET | cacheFlushBit,
Ttl: ttl,
},
A: ipv4,
}
list = append(list, a)
}
for _, ipv6 := range v6 {
aaaa := &dns.AAAA{
Hdr: dns.RR_Header{
Name: s.service.HostName,
Rrtype: dns.TypeAAAA,
Class: dns.ClassINET | cacheFlushBit,
Ttl: ttl,
},
AAAA: ipv6,
}
list = append(list, aaaa)
}
return list
}
func addrsForInterface(iface *net.Interface) ([]net.IP, []net.IP) {
var v4, v6, v6local []net.IP
addrs, _ := iface.Addrs()
for _, address := range addrs {
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
v4 = append(v4, ipnet.IP)
} else {
switch ip := ipnet.IP.To16(); ip != nil {
case ip.IsGlobalUnicast():
v6 = append(v6, ipnet.IP)
case ip.IsLinkLocalUnicast():
v6local = append(v6local, ipnet.IP)
}
}
}
}
if len(v6) == 0 {
v6 = v6local
}
return v4, v6
}
// unicastResponse is used to send a unicast response packet
func (s *Server) unicastResponse(resp *dns.Msg, ifIndex int, from net.Addr) error {
buf, err := resp.Pack()
if err != nil {
return err
}
addr := from.(*net.UDPAddr)
if addr.IP.To4() != nil {
if ifIndex != 0 {
var wcm ipv4.ControlMessage
wcm.IfIndex = ifIndex
_, err = s.ipv4conn.WriteTo(buf, &wcm, addr)
} else {
_, err = s.ipv4conn.WriteTo(buf, nil, addr)
}
return err
} else {
if ifIndex != 0 {
var wcm ipv6.ControlMessage
wcm.IfIndex = ifIndex
_, err = s.ipv6conn.WriteTo(buf, &wcm, addr)
} else {
_, err = s.ipv6conn.WriteTo(buf, nil, addr)
}
return err
}
}
// multicastResponse is used to send a multicast response packet
func (s *Server) multicastResponse(msg *dns.Msg, ifIndex int) error {
buf, err := msg.Pack()
if err != nil {
return fmt.Errorf("failed to pack msg %v: %w", msg, err)
}
if s.ipv4conn != nil {
// See https://pkg.go.dev/golang.org/x/net/ipv4#pkg-note-BUG
// As of Golang 1.18.4
// On Windows, the ControlMessage for ReadFrom and WriteTo methods of PacketConn is not implemented.
var wcm ipv4.ControlMessage
if ifIndex != 0 {
switch runtime.GOOS {
case "darwin", "ios", "linux":
wcm.IfIndex = ifIndex
default:
iface, _ := net.InterfaceByIndex(ifIndex)
if err := s.ipv4conn.SetMulticastInterface(iface); err != nil {
log.Printf("[WARN] mdns: Failed to set multicast interface: %v", err)
}
}
s.ipv4conn.WriteTo(buf, &wcm, ipv4Addr)
} else {
for _, intf := range s.ifaces {
switch runtime.GOOS {
case "darwin", "ios", "linux":
wcm.IfIndex = intf.Index
default:
if err := s.ipv4conn.SetMulticastInterface(&intf); err != nil {
log.Printf("[WARN] mdns: Failed to set multicast interface: %v", err)
}
}
s.ipv4conn.WriteTo(buf, &wcm, ipv4Addr)
}
}
}
if s.ipv6conn != nil {
// See https://pkg.go.dev/golang.org/x/net/ipv6#pkg-note-BUG
// As of Golang 1.18.4
// On Windows, the ControlMessage for ReadFrom and WriteTo methods of PacketConn is not implemented.
var wcm ipv6.ControlMessage
if ifIndex != 0 {
switch runtime.GOOS {
case "darwin", "ios", "linux":
wcm.IfIndex = ifIndex
default:
iface, _ := net.InterfaceByIndex(ifIndex)
if err := s.ipv6conn.SetMulticastInterface(iface); err != nil {
log.Printf("[WARN] mdns: Failed to set multicast interface: %v", err)
}
}
s.ipv6conn.WriteTo(buf, &wcm, ipv6Addr)
} else {
for _, intf := range s.ifaces {
switch runtime.GOOS {
case "darwin", "ios", "linux":
wcm.IfIndex = intf.Index
default:
if err := s.ipv6conn.SetMulticastInterface(&intf); err != nil {
log.Printf("[WARN] mdns: Failed to set multicast interface: %v", err)
}
}
s.ipv6conn.WriteTo(buf, &wcm, ipv6Addr)
}
}
}
return nil
}
func isUnicastQuestion(q dns.Question) bool {
// From RFC6762
// 18.12. Repurposing of Top Bit of qclass in Question Section
//
// In the Question Section of a Multicast DNS query, the top bit of the
// qclass field is used to indicate that unicast responses are preferred
// for this particular question. (See Section 5.4.)
return q.Qclass&qClassCacheFlush != 0
}

120
vendor/github.com/libp2p/zeroconf/v2/service.go generated vendored Normal file
View File

@@ -0,0 +1,120 @@
package zeroconf
import (
"fmt"
"net"
"sync"
"time"
)
// ServiceRecord contains the basic description of a service, which contains instance name, service type & domain
type ServiceRecord struct {
Instance string `json:"name"` // Instance name (e.g. "My web page")
Service string `json:"type"` // Service name (e.g. _http._tcp.)
Subtypes []string `json:"subtypes"` // Service subtypes
Domain string `json:"domain"` // If blank, assumes "local"
// private variable populated on ServiceRecord creation
serviceName string
serviceInstanceName string
serviceTypeName string
}
// ServiceName returns a complete service name (e.g. _foobar._tcp.local.), which is composed
// of a service name (also referred as service type) and a domain.
func (s *ServiceRecord) ServiceName() string {
return s.serviceName
}
// ServiceInstanceName returns a complete service instance name (e.g. MyDemo\ Service._foobar._tcp.local.),
// which is composed from service instance name, service name and a domain.
func (s *ServiceRecord) ServiceInstanceName() string {
return s.serviceInstanceName
}
// ServiceTypeName returns the complete identifier for a DNS-SD query.
func (s *ServiceRecord) ServiceTypeName() string {
return s.serviceTypeName
}
// newServiceRecord constructs a ServiceRecord.
func newServiceRecord(instance, service string, domain string) *ServiceRecord {
service, subtypes := parseSubtypes(service)
s := &ServiceRecord{
Instance: instance,
Service: service,
Domain: domain,
serviceName: fmt.Sprintf("%s.%s.", trimDot(service), trimDot(domain)),
}
for _, subtype := range subtypes {
s.Subtypes = append(s.Subtypes, fmt.Sprintf("%s._sub.%s", trimDot(subtype), s.serviceName))
}
// Cache service instance name
if instance != "" {
s.serviceInstanceName = fmt.Sprintf("%s.%s", trimDot(s.Instance), s.ServiceName())
}
// Cache service type name domain
typeNameDomain := "local"
if len(s.Domain) > 0 {
typeNameDomain = trimDot(s.Domain)
}
s.serviceTypeName = fmt.Sprintf("_services._dns-sd._udp.%s.", typeNameDomain)
return s
}
// lookupParams contains configurable properties to create a service discovery request
type lookupParams struct {
ServiceRecord
Entries chan<- *ServiceEntry // Entries Channel
isBrowsing bool
stopProbing chan struct{}
once sync.Once
}
// newLookupParams constructs a lookupParams.
func newLookupParams(instance, service, domain string, isBrowsing bool, entries chan<- *ServiceEntry) *lookupParams {
p := &lookupParams{
ServiceRecord: *newServiceRecord(instance, service, domain),
Entries: entries,
isBrowsing: isBrowsing,
}
if !isBrowsing {
p.stopProbing = make(chan struct{})
}
return p
}
// Notify subscriber that no more entries will arrive. Mostly caused
// by an expired context.
func (l *lookupParams) done() {
close(l.Entries)
}
func (l *lookupParams) disableProbing() {
l.once.Do(func() { close(l.stopProbing) })
}
// ServiceEntry represents a browse/lookup result for client API.
// It is also used to configure service registration (server API), which is
// used to answer multicast queries.
type ServiceEntry struct {
ServiceRecord
HostName string `json:"hostname"` // Host machine DNS name
Port int `json:"port"` // Service Port
Text []string `json:"text"` // Service info served as a TXT record
Expiry time.Time `json:"expiry"` // Expiry of the service entry, will be converted to a TTL value
AddrIPv4 []net.IP `json:"-"` // Host machine IPv4 address
AddrIPv6 []net.IP `json:"-"` // Host machine IPv6 address
}
// newServiceEntry constructs a ServiceEntry.
func newServiceEntry(instance, service string, domain string) *ServiceEntry {
return &ServiceEntry{
ServiceRecord: *newServiceRecord(instance, service, domain),
}
}

13
vendor/github.com/libp2p/zeroconf/v2/utils.go generated vendored Normal file
View File

@@ -0,0 +1,13 @@
package zeroconf
import "strings"
func parseSubtypes(service string) (string, []string) {
subtypes := strings.Split(service, ",")
return subtypes[0], subtypes[1:]
}
// trimDot is used to trim the dots from the start or end of a string
func trimDot(s string) string {
return strings.Trim(s, ".")
}