mirror of
https://github.com/bettercap/bettercap
synced 2025-07-16 10:03:39 -07:00
new: implemented mDNS server / spoofer (closes #542)
This commit is contained in:
parent
385c8e3926
commit
45951d2f82
13 changed files with 1237 additions and 1 deletions
9
Gopkg.lock
generated
9
Gopkg.lock
generated
|
@ -177,6 +177,14 @@
|
||||||
revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d"
|
revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d"
|
||||||
version = "v1.4.0"
|
version = "v1.4.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:89d41e902c8415f538d75e9d3008b286d30fe853ccf32def2dd37b82dd2ade14"
|
||||||
|
name = "github.com/hashicorp/mdns"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "UT"
|
||||||
|
revision = "06dd1a31b32c42d4d6c2cf8dbce70597d1118f54"
|
||||||
|
version = "v1.0.1"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
digest = "1:6480de9b8abc75bfb06947e139aa07429dfed37f95a258e90865c4c84a9e188b"
|
digest = "1:6480de9b8abc75bfb06947e139aa07429dfed37f95a258e90865c4c84a9e188b"
|
||||||
|
@ -377,6 +385,7 @@
|
||||||
"github.com/google/gousb",
|
"github.com/google/gousb",
|
||||||
"github.com/gorilla/mux",
|
"github.com/gorilla/mux",
|
||||||
"github.com/gorilla/websocket",
|
"github.com/gorilla/websocket",
|
||||||
|
"github.com/hashicorp/mdns",
|
||||||
"github.com/inconshreveable/go-vhost",
|
"github.com/inconshreveable/go-vhost",
|
||||||
"github.com/jpillora/go-tld",
|
"github.com/jpillora/go-tld",
|
||||||
"github.com/malfunkt/iprange",
|
"github.com/malfunkt/iprange",
|
||||||
|
|
160
modules/mdns_server/mdns_server.go
Normal file
160
modules/mdns_server/mdns_server.go
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
package mdns_server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/bettercap/bettercap/session"
|
||||||
|
|
||||||
|
"github.com/evilsocket/islazy/str"
|
||||||
|
"github.com/evilsocket/islazy/tui"
|
||||||
|
|
||||||
|
"github.com/hashicorp/mdns"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MDNSServer struct {
|
||||||
|
session.SessionModule
|
||||||
|
hostname string
|
||||||
|
instance string
|
||||||
|
service *mdns.MDNSService
|
||||||
|
server *mdns.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMDNSServer(s *session.Session) *MDNSServer {
|
||||||
|
host, _ := os.Hostname()
|
||||||
|
mod := &MDNSServer{
|
||||||
|
SessionModule: session.NewSessionModule("mdns.server", s),
|
||||||
|
hostname: host,
|
||||||
|
}
|
||||||
|
|
||||||
|
mod.AddParam(session.NewStringParameter("mdns.server.host",
|
||||||
|
mod.hostname+".",
|
||||||
|
"",
|
||||||
|
"mDNS hostname to advertise on the network."))
|
||||||
|
|
||||||
|
mod.AddParam(session.NewStringParameter("mdns.server.service",
|
||||||
|
"_companion-link._tcp.",
|
||||||
|
"",
|
||||||
|
"mDNS service name to advertise on the network."))
|
||||||
|
|
||||||
|
mod.AddParam(session.NewStringParameter("mdns.server.domain",
|
||||||
|
"local.",
|
||||||
|
"",
|
||||||
|
"mDNS domain."))
|
||||||
|
|
||||||
|
mod.AddParam(session.NewStringParameter("mdns.server.address",
|
||||||
|
session.ParamIfaceAddress,
|
||||||
|
session.IPv4Validator,
|
||||||
|
"IPv4 address of the mDNS service."))
|
||||||
|
|
||||||
|
mod.AddParam(session.NewStringParameter("mdns.server.address6",
|
||||||
|
session.ParamIfaceAddress6,
|
||||||
|
session.IPv6Validator,
|
||||||
|
"IPv6 address of the mDNS service."))
|
||||||
|
|
||||||
|
mod.AddParam(session.NewIntParameter("mdns.server.port",
|
||||||
|
"52377",
|
||||||
|
"Port of the mDNS service."))
|
||||||
|
|
||||||
|
mod.AddParam(session.NewStringParameter("mdns.server.info",
|
||||||
|
"rpBA=DE:AD:BE:EF:CA:FE, rpAD=abf99d4ff73f, rpHI=ec5fb3caf528, rpHN=20f8fb46e2eb, rpVr=164.16, rpHA=7406bd0eff69",
|
||||||
|
"",
|
||||||
|
"Comma separated list of informative TXT records for the mDNS server."))
|
||||||
|
|
||||||
|
mod.AddHandler(session.NewModuleHandler("mdns.server on", "",
|
||||||
|
"Start mDNS server.",
|
||||||
|
func(args []string) error {
|
||||||
|
return mod.Start()
|
||||||
|
}))
|
||||||
|
|
||||||
|
mod.AddHandler(session.NewModuleHandler("mdns.server off", "",
|
||||||
|
"Stop mDNS server.",
|
||||||
|
func(args []string) error {
|
||||||
|
return mod.Stop()
|
||||||
|
}))
|
||||||
|
|
||||||
|
return mod
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *MDNSServer) Name() string {
|
||||||
|
return "mdns.server"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *MDNSServer) Description() string {
|
||||||
|
return "A mDNS server module to create multicast services or spoof existing ones."
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *MDNSServer) Author() string {
|
||||||
|
return "Simone Margaritelli <evilsocket@gmail.com>"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *MDNSServer) Configure() (err error) {
|
||||||
|
if mod.Running() {
|
||||||
|
return session.ErrAlreadyStarted(mod.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
var host string
|
||||||
|
var service string
|
||||||
|
var domain string
|
||||||
|
var ip4 string
|
||||||
|
var ip6 string
|
||||||
|
var port int
|
||||||
|
var info string
|
||||||
|
|
||||||
|
if err, host = mod.StringParam("mdns.server.host"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err, service = mod.StringParam("mdns.server.service"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err, domain = mod.StringParam("mdns.server.domain"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err, ip4 = mod.StringParam("mdns.server.address"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err, ip6 = mod.StringParam("mdns.server.address6"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err, port = mod.IntParam("mdns.server.port"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err, info = mod.StringParam("mdns.server.info"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.SetOutput(ioutil.Discard)
|
||||||
|
|
||||||
|
mod.instance = fmt.Sprintf("%s%s%s", host, service, domain)
|
||||||
|
mod.service, err = mdns.NewMDNSService(
|
||||||
|
mod.instance,
|
||||||
|
service,
|
||||||
|
domain,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
[]net.IP{
|
||||||
|
net.ParseIP(ip4),
|
||||||
|
net.ParseIP(ip6),
|
||||||
|
},
|
||||||
|
str.Comma(info))
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *MDNSServer) Start() error {
|
||||||
|
if err := mod.Configure(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mod.SetRunning(true, func() {
|
||||||
|
var err error
|
||||||
|
mod.Info("advertising service %s -> %s:%d", tui.Bold(mod.instance), mod.service.IPs, mod.service.Port)
|
||||||
|
if mod.server, err = mdns.NewServer(&mdns.Config{Zone: mod.service}); err != nil {
|
||||||
|
mod.Error("%v", err)
|
||||||
|
mod.Stop()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod *MDNSServer) Stop() error {
|
||||||
|
return mod.SetRunning(false, func() {
|
||||||
|
mod.server.Shutdown()
|
||||||
|
})
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/bettercap/bettercap/modules/https_proxy"
|
"github.com/bettercap/bettercap/modules/https_proxy"
|
||||||
"github.com/bettercap/bettercap/modules/https_server"
|
"github.com/bettercap/bettercap/modules/https_server"
|
||||||
"github.com/bettercap/bettercap/modules/mac_changer"
|
"github.com/bettercap/bettercap/modules/mac_changer"
|
||||||
|
"github.com/bettercap/bettercap/modules/mdns_server"
|
||||||
"github.com/bettercap/bettercap/modules/mysql_server"
|
"github.com/bettercap/bettercap/modules/mysql_server"
|
||||||
"github.com/bettercap/bettercap/modules/net_probe"
|
"github.com/bettercap/bettercap/modules/net_probe"
|
||||||
"github.com/bettercap/bettercap/modules/net_recon"
|
"github.com/bettercap/bettercap/modules/net_recon"
|
||||||
|
@ -48,6 +49,7 @@ func LoadModules(sess *session.Session) {
|
||||||
sess.Register(https_server.NewHttpsServer(sess))
|
sess.Register(https_server.NewHttpsServer(sess))
|
||||||
sess.Register(mac_changer.NewMacChanger(sess))
|
sess.Register(mac_changer.NewMacChanger(sess))
|
||||||
sess.Register(mysql_server.NewMySQLServer(sess))
|
sess.Register(mysql_server.NewMySQLServer(sess))
|
||||||
|
sess.Register(mdns_server.NewMDNSServer(sess))
|
||||||
sess.Register(net_sniff.NewSniffer(sess))
|
sess.Register(net_sniff.NewSniffer(sess))
|
||||||
sess.Register(packet_proxy.NewPacketProxy(sess))
|
sess.Register(packet_proxy.NewPacketProxy(sess))
|
||||||
sess.Register(net_probe.NewProber(sess))
|
sess.Register(net_probe.NewProber(sess))
|
||||||
|
|
|
@ -13,7 +13,10 @@ import (
|
||||||
"github.com/bettercap/readline"
|
"github.com/bettercap/readline"
|
||||||
)
|
)
|
||||||
|
|
||||||
const IPv4Validator = `^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$`
|
const (
|
||||||
|
IPv4Validator = `^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$`
|
||||||
|
IPv6Validator = `^[:a-fA-F0-9]{6,}$`
|
||||||
|
)
|
||||||
|
|
||||||
type ModuleHandler struct {
|
type ModuleHandler struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
|
|
|
@ -94,6 +94,7 @@ func (p ModuleParam) Validate(value string) (error, interface{}) {
|
||||||
|
|
||||||
const ParamIfaceName = "<interface name>"
|
const ParamIfaceName = "<interface name>"
|
||||||
const ParamIfaceAddress = "<interface address>"
|
const ParamIfaceAddress = "<interface address>"
|
||||||
|
const ParamIfaceAddress6 = "<interface address6>"
|
||||||
const ParamSubnet = "<entire subnet>"
|
const ParamSubnet = "<entire subnet>"
|
||||||
const ParamRandomMAC = "<random mac>"
|
const ParamRandomMAC = "<random mac>"
|
||||||
|
|
||||||
|
@ -103,6 +104,8 @@ func (p ModuleParam) parse(s *Session, v string) string {
|
||||||
v = s.Interface.Name()
|
v = s.Interface.Name()
|
||||||
case ParamIfaceAddress:
|
case ParamIfaceAddress:
|
||||||
v = s.Interface.IpAddress
|
v = s.Interface.IpAddress
|
||||||
|
case ParamIfaceAddress6:
|
||||||
|
v = s.Interface.Ip6Address
|
||||||
case ParamSubnet:
|
case ParamSubnet:
|
||||||
v = s.Interface.CIDR()
|
v = s.Interface.CIDR()
|
||||||
case ParamRandomMAC:
|
case ParamRandomMAC:
|
||||||
|
|
23
vendor/github.com/hashicorp/mdns/.gitignore
generated
vendored
Normal file
23
vendor/github.com/hashicorp/mdns/.gitignore
generated
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# 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
|
20
vendor/github.com/hashicorp/mdns/LICENSE
generated
vendored
Normal file
20
vendor/github.com/hashicorp/mdns/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Armon Dadgar
|
||||||
|
|
||||||
|
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.
|
37
vendor/github.com/hashicorp/mdns/README.md
generated
vendored
Normal file
37
vendor/github.com/hashicorp/mdns/README.md
generated
vendored
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
mdns
|
||||||
|
====
|
||||||
|
|
||||||
|
Simple mDNS client/server library in Golang. mDNS or Multicast DNS can be
|
||||||
|
used to discover services on the local network without the use of an authoritative
|
||||||
|
DNS server. This enables peer-to-peer discovery. It is important to note that many
|
||||||
|
networks restrict the use of multicasting, which prevents mDNS from functioning.
|
||||||
|
Notably, multicast cannot be used in any sort of cloud, or shared infrastructure
|
||||||
|
environment. However it works well in most office, home, or private infrastructure
|
||||||
|
environments.
|
||||||
|
|
||||||
|
Using the library is very simple, here is an example of publishing a service entry:
|
||||||
|
|
||||||
|
// Setup our service export
|
||||||
|
host, _ := os.Hostname()
|
||||||
|
info := []string{"My awesome service"},
|
||||||
|
service, _ := NewMDNSService(host, "_foobar._tcp", "", "", 8000, nil, info)
|
||||||
|
|
||||||
|
// Create the mDNS server, defer shutdown
|
||||||
|
server, _ := mdns.NewServer(&mdns.Config{Zone: service})
|
||||||
|
defer server.Shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
Doing a lookup for service providers is also very simple:
|
||||||
|
|
||||||
|
// Make a channel for results and start listening
|
||||||
|
entriesCh := make(chan *mdns.ServiceEntry, 4)
|
||||||
|
go func() {
|
||||||
|
for entry := range entriesCh {
|
||||||
|
fmt.Printf("Got new entry: %v\n", entry)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Start the lookup
|
||||||
|
mdns.Lookup("_foobar._tcp", entriesCh)
|
||||||
|
close(entriesCh)
|
||||||
|
|
365
vendor/github.com/hashicorp/mdns/client.go
generated
vendored
Normal file
365
vendor/github.com/hashicorp/mdns/client.go
generated
vendored
Normal file
|
@ -0,0 +1,365 @@
|
||||||
|
package mdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"golang.org/x/net/ipv4"
|
||||||
|
"golang.org/x/net/ipv6"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServiceEntry is returned after we query for a service
|
||||||
|
type ServiceEntry struct {
|
||||||
|
Name string
|
||||||
|
Host string
|
||||||
|
AddrV4 net.IP
|
||||||
|
AddrV6 net.IP
|
||||||
|
Port int
|
||||||
|
Info string
|
||||||
|
InfoFields []string
|
||||||
|
|
||||||
|
Addr net.IP // @Deprecated
|
||||||
|
|
||||||
|
hasTXT bool
|
||||||
|
sent bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// complete is used to check if we have all the info we need
|
||||||
|
func (s *ServiceEntry) complete() bool {
|
||||||
|
return (s.AddrV4 != nil || s.AddrV6 != nil || s.Addr != nil) && s.Port != 0 && s.hasTXT
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryParam is used to customize how a Lookup is performed
|
||||||
|
type QueryParam struct {
|
||||||
|
Service string // Service to lookup
|
||||||
|
Domain string // Lookup domain, default "local"
|
||||||
|
Timeout time.Duration // Lookup timeout, default 1 second
|
||||||
|
Interface *net.Interface // Multicast interface to use
|
||||||
|
Entries chan<- *ServiceEntry // Entries Channel
|
||||||
|
WantUnicastResponse bool // Unicast response desired, as per 5.4 in RFC
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultParams is used to return a default set of QueryParam's
|
||||||
|
func DefaultParams(service string) *QueryParam {
|
||||||
|
return &QueryParam{
|
||||||
|
Service: service,
|
||||||
|
Domain: "local",
|
||||||
|
Timeout: time.Second,
|
||||||
|
Entries: make(chan *ServiceEntry),
|
||||||
|
WantUnicastResponse: false, // TODO(reddaly): Change this default.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query looks up a given service, in a domain, waiting at most
|
||||||
|
// for a timeout before finishing the query. The results are streamed
|
||||||
|
// to a channel. Sends will not block, so clients should make sure to
|
||||||
|
// either read or buffer.
|
||||||
|
func Query(params *QueryParam) error {
|
||||||
|
// Create a new client
|
||||||
|
client, err := newClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// Set the multicast interface
|
||||||
|
if params.Interface != nil {
|
||||||
|
if err := client.setInterface(params.Interface); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure defaults are set
|
||||||
|
if params.Domain == "" {
|
||||||
|
params.Domain = "local"
|
||||||
|
}
|
||||||
|
if params.Timeout == 0 {
|
||||||
|
params.Timeout = time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the query
|
||||||
|
return client.query(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup is the same as Query, however it uses all the default parameters
|
||||||
|
func Lookup(service string, entries chan<- *ServiceEntry) error {
|
||||||
|
params := DefaultParams(service)
|
||||||
|
params.Entries = entries
|
||||||
|
return Query(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client provides a query interface that can be used to
|
||||||
|
// search for service providers using mDNS
|
||||||
|
type client struct {
|
||||||
|
ipv4UnicastConn *net.UDPConn
|
||||||
|
ipv6UnicastConn *net.UDPConn
|
||||||
|
|
||||||
|
ipv4MulticastConn *net.UDPConn
|
||||||
|
ipv6MulticastConn *net.UDPConn
|
||||||
|
|
||||||
|
closed int32
|
||||||
|
closedCh chan struct{} // TODO(reddaly): This doesn't appear to be used.
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new mdns Client that can be used to query
|
||||||
|
// for records
|
||||||
|
func newClient() (*client, error) {
|
||||||
|
// TODO(reddaly): At least attempt to bind to the port required in the spec.
|
||||||
|
// Create a IPv4 listener
|
||||||
|
uconn4, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERR] mdns: Failed to bind to udp4 port: %v", err)
|
||||||
|
}
|
||||||
|
uconn6, err := net.ListenUDP("udp6", &net.UDPAddr{IP: net.IPv6zero, Port: 0})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERR] mdns: Failed to bind to udp6 port: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if uconn4 == nil && uconn6 == nil {
|
||||||
|
return nil, fmt.Errorf("failed to bind to any unicast udp port")
|
||||||
|
}
|
||||||
|
|
||||||
|
mconn4, err := net.ListenMulticastUDP("udp4", nil, ipv4Addr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERR] mdns: Failed to bind to udp4 port: %v", err)
|
||||||
|
}
|
||||||
|
mconn6, err := net.ListenMulticastUDP("udp6", nil, ipv6Addr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERR] mdns: Failed to bind to udp6 port: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mconn4 == nil && mconn6 == nil {
|
||||||
|
return nil, fmt.Errorf("failed to bind to any multicast udp port")
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &client{
|
||||||
|
ipv4MulticastConn: mconn4,
|
||||||
|
ipv6MulticastConn: mconn6,
|
||||||
|
ipv4UnicastConn: uconn4,
|
||||||
|
ipv6UnicastConn: uconn6,
|
||||||
|
closedCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close is used to cleanup the client
|
||||||
|
func (c *client) Close() error {
|
||||||
|
if !atomic.CompareAndSwapInt32(&c.closed, 0, 1) {
|
||||||
|
// something else already closed it
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[INFO] mdns: Closing client %v", *c)
|
||||||
|
close(c.closedCh)
|
||||||
|
|
||||||
|
if c.ipv4UnicastConn != nil {
|
||||||
|
c.ipv4UnicastConn.Close()
|
||||||
|
}
|
||||||
|
if c.ipv6UnicastConn != nil {
|
||||||
|
c.ipv6UnicastConn.Close()
|
||||||
|
}
|
||||||
|
if c.ipv4MulticastConn != nil {
|
||||||
|
c.ipv4MulticastConn.Close()
|
||||||
|
}
|
||||||
|
if c.ipv6MulticastConn != nil {
|
||||||
|
c.ipv6MulticastConn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setInterface is used to set the query interface, uses sytem
|
||||||
|
// default if not provided
|
||||||
|
func (c *client) setInterface(iface *net.Interface) error {
|
||||||
|
p := ipv4.NewPacketConn(c.ipv4UnicastConn)
|
||||||
|
if err := p.SetMulticastInterface(iface); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p2 := ipv6.NewPacketConn(c.ipv6UnicastConn)
|
||||||
|
if err := p2.SetMulticastInterface(iface); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p = ipv4.NewPacketConn(c.ipv4MulticastConn)
|
||||||
|
if err := p.SetMulticastInterface(iface); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p2 = ipv6.NewPacketConn(c.ipv6MulticastConn)
|
||||||
|
if err := p2.SetMulticastInterface(iface); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// query is used to perform a lookup and stream results
|
||||||
|
func (c *client) query(params *QueryParam) error {
|
||||||
|
// Create the service name
|
||||||
|
serviceAddr := fmt.Sprintf("%s.%s.", trimDot(params.Service), trimDot(params.Domain))
|
||||||
|
|
||||||
|
// Start listening for response packets
|
||||||
|
msgCh := make(chan *dns.Msg, 32)
|
||||||
|
go c.recv(c.ipv4UnicastConn, msgCh)
|
||||||
|
go c.recv(c.ipv6UnicastConn, msgCh)
|
||||||
|
go c.recv(c.ipv4MulticastConn, msgCh)
|
||||||
|
go c.recv(c.ipv6MulticastConn, msgCh)
|
||||||
|
|
||||||
|
// Send the query
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(serviceAddr, dns.TypePTR)
|
||||||
|
// RFC 6762, section 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.)
|
||||||
|
if params.WantUnicastResponse {
|
||||||
|
m.Question[0].Qclass |= 1 << 15
|
||||||
|
}
|
||||||
|
m.RecursionDesired = false
|
||||||
|
if err := c.sendQuery(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map the in-progress responses
|
||||||
|
inprogress := make(map[string]*ServiceEntry)
|
||||||
|
|
||||||
|
// Listen until we reach the timeout
|
||||||
|
finish := time.After(params.Timeout)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case resp := <-msgCh:
|
||||||
|
var inp *ServiceEntry
|
||||||
|
for _, answer := range append(resp.Answer, resp.Extra...) {
|
||||||
|
// TODO(reddaly): Check that response corresponds to serviceAddr?
|
||||||
|
switch rr := answer.(type) {
|
||||||
|
case *dns.PTR:
|
||||||
|
// Create new entry for this
|
||||||
|
inp = ensureName(inprogress, rr.Ptr)
|
||||||
|
|
||||||
|
case *dns.SRV:
|
||||||
|
// Check for a target mismatch
|
||||||
|
if rr.Target != rr.Hdr.Name {
|
||||||
|
alias(inprogress, rr.Hdr.Name, rr.Target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the port
|
||||||
|
inp = ensureName(inprogress, rr.Hdr.Name)
|
||||||
|
inp.Host = rr.Target
|
||||||
|
inp.Port = int(rr.Port)
|
||||||
|
|
||||||
|
case *dns.TXT:
|
||||||
|
// Pull out the txt
|
||||||
|
inp = ensureName(inprogress, rr.Hdr.Name)
|
||||||
|
inp.Info = strings.Join(rr.Txt, "|")
|
||||||
|
inp.InfoFields = rr.Txt
|
||||||
|
inp.hasTXT = true
|
||||||
|
|
||||||
|
case *dns.A:
|
||||||
|
// Pull out the IP
|
||||||
|
inp = ensureName(inprogress, rr.Hdr.Name)
|
||||||
|
inp.Addr = rr.A // @Deprecated
|
||||||
|
inp.AddrV4 = rr.A
|
||||||
|
|
||||||
|
case *dns.AAAA:
|
||||||
|
// Pull out the IP
|
||||||
|
inp = ensureName(inprogress, rr.Hdr.Name)
|
||||||
|
inp.Addr = rr.AAAA // @Deprecated
|
||||||
|
inp.AddrV6 = rr.AAAA
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if inp == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this entry is complete
|
||||||
|
if inp.complete() {
|
||||||
|
if inp.sent {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
inp.sent = true
|
||||||
|
select {
|
||||||
|
case params.Entries <- inp:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fire off a node specific query
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(inp.Name, dns.TypePTR)
|
||||||
|
m.RecursionDesired = false
|
||||||
|
if err := c.sendQuery(m); err != nil {
|
||||||
|
log.Printf("[ERR] mdns: Failed to query instance %s: %v", inp.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case <-finish:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendQuery is used to multicast a query out
|
||||||
|
func (c *client) sendQuery(q *dns.Msg) error {
|
||||||
|
buf, err := q.Pack()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if c.ipv4UnicastConn != nil {
|
||||||
|
c.ipv4UnicastConn.WriteToUDP(buf, ipv4Addr)
|
||||||
|
}
|
||||||
|
if c.ipv6UnicastConn != nil {
|
||||||
|
c.ipv6UnicastConn.WriteToUDP(buf, ipv6Addr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// recv is used to receive until we get a shutdown
|
||||||
|
func (c *client) recv(l *net.UDPConn, msgCh chan *dns.Msg) {
|
||||||
|
if l == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf := make([]byte, 65536)
|
||||||
|
for atomic.LoadInt32(&c.closed) == 0 {
|
||||||
|
n, err := l.Read(buf)
|
||||||
|
|
||||||
|
if atomic.LoadInt32(&c.closed) == 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERR] mdns: Failed to read packet: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
msg := new(dns.Msg)
|
||||||
|
if err := msg.Unpack(buf[:n]); err != nil {
|
||||||
|
log.Printf("[ERR] mdns: Failed to unpack packet: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case msgCh <- msg:
|
||||||
|
case <-c.closedCh:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureName is used to ensure the named node is in progress
|
||||||
|
func ensureName(inprogress map[string]*ServiceEntry, name string) *ServiceEntry {
|
||||||
|
if inp, ok := inprogress[name]; ok {
|
||||||
|
return inp
|
||||||
|
}
|
||||||
|
inp := &ServiceEntry{
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
inprogress[name] = inp
|
||||||
|
return inp
|
||||||
|
}
|
||||||
|
|
||||||
|
// alias is used to setup an alias between two entries
|
||||||
|
func alias(inprogress map[string]*ServiceEntry, src, dst string) {
|
||||||
|
srcEntry := ensureName(inprogress, src)
|
||||||
|
inprogress[dst] = srcEntry
|
||||||
|
}
|
9
vendor/github.com/hashicorp/mdns/go.mod
generated
vendored
Normal file
9
vendor/github.com/hashicorp/mdns/go.mod
generated
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
module github.com/hashicorp/mdns
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/miekg/dns v1.0.14
|
||||||
|
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3 // indirect
|
||||||
|
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5 // indirect
|
||||||
|
)
|
10
vendor/github.com/hashicorp/mdns/go.sum
generated
vendored
Normal file
10
vendor/github.com/hashicorp/mdns/go.sum
generated
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA=
|
||||||
|
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||||
|
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3 h1:KYQXGkl6vs02hK7pK4eIbw0NpNPedieTSTEiJ//bwGs=
|
||||||
|
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519 h1:x6rhz8Y9CjbgQkccRGmELH6K+LJj7tOoh3XWeC1yaQM=
|
||||||
|
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5 h1:x6r4Jo0KNzOOzYd8lbcRsqjuqEASK6ob3auvWYM4/8U=
|
||||||
|
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
288
vendor/github.com/hashicorp/mdns/server.go
generated
vendored
Normal file
288
vendor/github.com/hashicorp/mdns/server.go
generated
vendored
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
package mdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ipv4mdns = "224.0.0.251"
|
||||||
|
ipv6mdns = "ff02::fb"
|
||||||
|
mdnsPort = 5353
|
||||||
|
forceUnicastResponses = false
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ipv4Addr = &net.UDPAddr{
|
||||||
|
IP: net.ParseIP(ipv4mdns),
|
||||||
|
Port: mdnsPort,
|
||||||
|
}
|
||||||
|
ipv6Addr = &net.UDPAddr{
|
||||||
|
IP: net.ParseIP(ipv6mdns),
|
||||||
|
Port: mdnsPort,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config is used to configure the mDNS server
|
||||||
|
type Config struct {
|
||||||
|
// Zone must be provided to support responding to queries
|
||||||
|
Zone Zone
|
||||||
|
|
||||||
|
// Iface if provided binds the multicast listener to the given
|
||||||
|
// interface. If not provided, the system default multicase interface
|
||||||
|
// is used.
|
||||||
|
Iface *net.Interface
|
||||||
|
|
||||||
|
// LogEmptyResponses indicates the server should print an informative message
|
||||||
|
// when there is an mDNS query for which the server has no response.
|
||||||
|
LogEmptyResponses bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// mDNS server is used to listen for mDNS queries and respond if we
|
||||||
|
// have a matching local record
|
||||||
|
type Server struct {
|
||||||
|
config *Config
|
||||||
|
|
||||||
|
ipv4List *net.UDPConn
|
||||||
|
ipv6List *net.UDPConn
|
||||||
|
|
||||||
|
shutdown int32
|
||||||
|
shutdownCh chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServer is used to create a new mDNS server from a config
|
||||||
|
func NewServer(config *Config) (*Server, error) {
|
||||||
|
// Create the listeners
|
||||||
|
ipv4List, _ := net.ListenMulticastUDP("udp4", config.Iface, ipv4Addr)
|
||||||
|
ipv6List, _ := net.ListenMulticastUDP("udp6", config.Iface, ipv6Addr)
|
||||||
|
|
||||||
|
// Check if we have any listener
|
||||||
|
if ipv4List == nil && ipv6List == nil {
|
||||||
|
return nil, fmt.Errorf("No multicast listeners could be started")
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &Server{
|
||||||
|
config: config,
|
||||||
|
ipv4List: ipv4List,
|
||||||
|
ipv6List: ipv6List,
|
||||||
|
shutdownCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
if ipv4List != nil {
|
||||||
|
go s.recv(s.ipv4List)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ipv6List != nil {
|
||||||
|
go s.recv(s.ipv6List)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown is used to shutdown the listener
|
||||||
|
func (s *Server) Shutdown() error {
|
||||||
|
if !atomic.CompareAndSwapInt32(&s.shutdown, 0, 1) {
|
||||||
|
// something else already closed us
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
close(s.shutdownCh)
|
||||||
|
|
||||||
|
if s.ipv4List != nil {
|
||||||
|
s.ipv4List.Close()
|
||||||
|
}
|
||||||
|
if s.ipv6List != nil {
|
||||||
|
s.ipv6List.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// recv is a long running routine to receive packets from an interface
|
||||||
|
func (s *Server) recv(c *net.UDPConn) {
|
||||||
|
if c == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf := make([]byte, 65536)
|
||||||
|
for atomic.LoadInt32(&s.shutdown) == 0 {
|
||||||
|
n, from, err := c.ReadFrom(buf)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := s.parsePacket(buf[:n], from); err != nil {
|
||||||
|
log.Printf("[ERR] mdns: Failed to handle query: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePacket is used to parse an incoming packet
|
||||||
|
func (s *Server) parsePacket(packet []byte, from net.Addr) error {
|
||||||
|
var msg dns.Msg
|
||||||
|
if err := msg.Unpack(packet); err != nil {
|
||||||
|
log.Printf("[ERR] mdns: Failed to unpack packet: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.handleQuery(&msg, from)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleQuery is used to handle an incoming query
|
||||||
|
func (s *Server) handleQuery(query *dns.Msg, from net.Addr) error {
|
||||||
|
if query.Opcode != dns.OpcodeQuery {
|
||||||
|
// "In both multicast query and multicast response messages, the OPCODE MUST
|
||||||
|
// be zero on transmission (only standard queries are currently supported
|
||||||
|
// over multicast). Multicast DNS messages received with an OPCODE other
|
||||||
|
// than zero MUST be silently ignored." Note: OpcodeQuery == 0
|
||||||
|
return fmt.Errorf("mdns: received query with non-zero Opcode %v: %v", query.Opcode, *query)
|
||||||
|
}
|
||||||
|
if query.Rcode != 0 {
|
||||||
|
// "In both multicast query and multicast response messages, the Response
|
||||||
|
// Code MUST be zero on transmission. Multicast DNS messages received with
|
||||||
|
// non-zero Response Codes MUST be silently ignored."
|
||||||
|
return fmt.Errorf("mdns: received query with non-zero Rcode %v: %v", query.Rcode, *query)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(reddaly): Handle "TC (Truncated) Bit":
|
||||||
|
// In query messages, if the TC bit is set, it means that additional
|
||||||
|
// Known-Answer records may be following shortly. A responder SHOULD
|
||||||
|
// record this fact, and wait for those additional Known-Answer records,
|
||||||
|
// before deciding whether to respond. If the TC bit is clear, it means
|
||||||
|
// that the querying host has no additional Known Answers.
|
||||||
|
if query.Truncated {
|
||||||
|
return fmt.Errorf("[ERR] mdns: support for DNS requests with high truncated bit not implemented: %v", *query)
|
||||||
|
}
|
||||||
|
|
||||||
|
var unicastAnswer, multicastAnswer []dns.RR
|
||||||
|
|
||||||
|
// Handle each question
|
||||||
|
for _, q := range query.Question {
|
||||||
|
mrecs, urecs := s.handleQuestion(q)
|
||||||
|
multicastAnswer = append(multicastAnswer, mrecs...)
|
||||||
|
unicastAnswer = append(unicastAnswer, urecs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// See section 18 of RFC 6762 for rules about DNS headers.
|
||||||
|
resp := func(unicast bool) *dns.Msg {
|
||||||
|
// 18.1: ID (Query Identifier)
|
||||||
|
// 0 for multicast response, query.Id for unicast response
|
||||||
|
id := uint16(0)
|
||||||
|
if unicast {
|
||||||
|
id = query.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
var answer []dns.RR
|
||||||
|
if unicast {
|
||||||
|
answer = unicastAnswer
|
||||||
|
} else {
|
||||||
|
answer = multicastAnswer
|
||||||
|
}
|
||||||
|
if len(answer) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dns.Msg{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
Id: id,
|
||||||
|
|
||||||
|
// 18.2: QR (Query/Response) Bit - must be set to 1 in response.
|
||||||
|
Response: true,
|
||||||
|
|
||||||
|
// 18.3: OPCODE - must be zero in response (OpcodeQuery == 0)
|
||||||
|
Opcode: dns.OpcodeQuery,
|
||||||
|
|
||||||
|
// 18.4: AA (Authoritative Answer) Bit - must be set to 1
|
||||||
|
Authoritative: true,
|
||||||
|
|
||||||
|
// The following fields must all be set to 0:
|
||||||
|
// 18.5: TC (TRUNCATED) Bit
|
||||||
|
// 18.6: RD (Recursion Desired) Bit
|
||||||
|
// 18.7: RA (Recursion Available) Bit
|
||||||
|
// 18.8: Z (Zero) Bit
|
||||||
|
// 18.9: AD (Authentic Data) Bit
|
||||||
|
// 18.10: CD (Checking Disabled) Bit
|
||||||
|
// 18.11: RCODE (Response Code)
|
||||||
|
},
|
||||||
|
// 18.12 pertains to questions (handled by handleQuestion)
|
||||||
|
// 18.13 pertains to resource records (handled by handleQuestion)
|
||||||
|
|
||||||
|
// 18.14: Name Compression - responses should be compressed (though see
|
||||||
|
// caveats in the RFC), so set the Compress bit (part of the dns library
|
||||||
|
// API, not part of the DNS packet) to true.
|
||||||
|
Compress: true,
|
||||||
|
|
||||||
|
Answer: answer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.config.LogEmptyResponses && len(multicastAnswer) == 0 && len(unicastAnswer) == 0 {
|
||||||
|
questions := make([]string, len(query.Question))
|
||||||
|
for i, q := range query.Question {
|
||||||
|
questions[i] = q.Name
|
||||||
|
}
|
||||||
|
log.Printf("no responses for query with questions: %s", strings.Join(questions, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if mresp := resp(false); mresp != nil {
|
||||||
|
if err := s.sendResponse(mresp, from, false); err != nil {
|
||||||
|
return fmt.Errorf("mdns: error sending multicast response: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if uresp := resp(true); uresp != nil {
|
||||||
|
if err := s.sendResponse(uresp, from, true); err != nil {
|
||||||
|
return fmt.Errorf("mdns: error sending unicast response: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleQuestion is used to handle an incoming question
|
||||||
|
//
|
||||||
|
// The response to a question may be transmitted over multicast, unicast, or
|
||||||
|
// both. The return values are DNS records for each transmission type.
|
||||||
|
func (s *Server) handleQuestion(q dns.Question) (multicastRecs, unicastRecs []dns.RR) {
|
||||||
|
records := s.config.Zone.Records(q)
|
||||||
|
|
||||||
|
if len(records) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle unicast and multicast responses.
|
||||||
|
// TODO(reddaly): The decision about sending over unicast vs. multicast is not
|
||||||
|
// yet fully compliant with RFC 6762. For example, the unicast bit should be
|
||||||
|
// ignored if the records in question are close to TTL expiration. For now,
|
||||||
|
// we just use the unicast bit to make the decision, as per the spec:
|
||||||
|
// RFC 6762, section 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.)
|
||||||
|
if q.Qclass&(1<<15) != 0 || forceUnicastResponses {
|
||||||
|
return nil, records
|
||||||
|
}
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendResponse is used to send a response packet
|
||||||
|
func (s *Server) sendResponse(resp *dns.Msg, from net.Addr, unicast bool) error {
|
||||||
|
// TODO(reddaly): Respect the unicast argument, and allow sending responses
|
||||||
|
// over multicast.
|
||||||
|
buf, err := resp.Pack()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the socket to send from
|
||||||
|
addr := from.(*net.UDPAddr)
|
||||||
|
if addr.IP.To4() != nil {
|
||||||
|
_, err = s.ipv4List.WriteToUDP(buf, addr)
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
_, err = s.ipv6List.WriteToUDP(buf, addr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
307
vendor/github.com/hashicorp/mdns/zone.go
generated
vendored
Normal file
307
vendor/github.com/hashicorp/mdns/zone.go
generated
vendored
Normal file
|
@ -0,0 +1,307 @@
|
||||||
|
package mdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// defaultTTL is the default TTL value in returned DNS records in seconds.
|
||||||
|
defaultTTL = 120
|
||||||
|
)
|
||||||
|
|
||||||
|
// Zone is the interface used to integrate with the server and
|
||||||
|
// to serve records dynamically
|
||||||
|
type Zone interface {
|
||||||
|
// Records returns DNS records in response to a DNS question.
|
||||||
|
Records(q dns.Question) []dns.RR
|
||||||
|
}
|
||||||
|
|
||||||
|
// MDNSService is used to export a named service by implementing a Zone
|
||||||
|
type MDNSService struct {
|
||||||
|
Instance string // Instance name (e.g. "hostService name")
|
||||||
|
Service string // Service name (e.g. "_http._tcp.")
|
||||||
|
Domain string // If blank, assumes "local"
|
||||||
|
HostName string // Host machine DNS name (e.g. "mymachine.net.")
|
||||||
|
Port int // Service Port
|
||||||
|
IPs []net.IP // IP addresses for the service's host
|
||||||
|
TXT []string // Service TXT records
|
||||||
|
|
||||||
|
serviceAddr string // Fully qualified service address
|
||||||
|
instanceAddr string // Fully qualified instance address
|
||||||
|
enumAddr string // _services._dns-sd._udp.<domain>
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateFQDN returns an error if the passed string is not a fully qualified
|
||||||
|
// hdomain name (more specifically, a hostname).
|
||||||
|
func validateFQDN(s string) error {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return fmt.Errorf("FQDN must not be blank")
|
||||||
|
}
|
||||||
|
if s[len(s)-1] != '.' {
|
||||||
|
return fmt.Errorf("FQDN must end in period: %s", s)
|
||||||
|
}
|
||||||
|
// TODO(reddaly): Perform full validation.
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMDNSService returns a new instance of MDNSService.
|
||||||
|
//
|
||||||
|
// If domain, hostName, or ips is set to the zero value, then a default value
|
||||||
|
// will be inferred from the operating system.
|
||||||
|
//
|
||||||
|
// TODO(reddaly): This interface may need to change to account for "unique
|
||||||
|
// record" conflict rules of the mDNS protocol. Upon startup, the server should
|
||||||
|
// check to ensure that the instance name does not conflict with other instance
|
||||||
|
// names, and, if required, select a new name. There may also be conflicting
|
||||||
|
// hostName A/AAAA records.
|
||||||
|
func NewMDNSService(instance, service, domain, hostName string, port int, ips []net.IP, txt []string) (*MDNSService, error) {
|
||||||
|
// Sanity check inputs
|
||||||
|
if instance == "" {
|
||||||
|
return nil, fmt.Errorf("missing service instance name")
|
||||||
|
}
|
||||||
|
if service == "" {
|
||||||
|
return nil, fmt.Errorf("missing service name")
|
||||||
|
}
|
||||||
|
if port == 0 {
|
||||||
|
return nil, fmt.Errorf("missing service port")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default domain
|
||||||
|
if domain == "" {
|
||||||
|
domain = "local."
|
||||||
|
}
|
||||||
|
if err := validateFQDN(domain); err != nil {
|
||||||
|
return nil, fmt.Errorf("domain %q is not a fully-qualified domain name: %v", domain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get host information if no host is specified.
|
||||||
|
if hostName == "" {
|
||||||
|
var err error
|
||||||
|
hostName, err = os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not determine host: %v", err)
|
||||||
|
}
|
||||||
|
hostName = fmt.Sprintf("%s.", hostName)
|
||||||
|
}
|
||||||
|
if err := validateFQDN(hostName); err != nil {
|
||||||
|
return nil, fmt.Errorf("hostName %q is not a fully-qualified domain name: %v", hostName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ips) == 0 {
|
||||||
|
var err error
|
||||||
|
ips, err = net.LookupIP(hostName)
|
||||||
|
if err != nil {
|
||||||
|
// Try appending the host domain suffix and lookup again
|
||||||
|
// (required for Linux-based hosts)
|
||||||
|
tmpHostName := fmt.Sprintf("%s%s", hostName, domain)
|
||||||
|
|
||||||
|
ips, err = net.LookupIP(tmpHostName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not determine host IP addresses for %s", hostName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ip := range ips {
|
||||||
|
if ip.To4() == nil && ip.To16() == nil {
|
||||||
|
return nil, fmt.Errorf("invalid IP address in IPs list: %v", ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MDNSService{
|
||||||
|
Instance: instance,
|
||||||
|
Service: service,
|
||||||
|
Domain: domain,
|
||||||
|
HostName: hostName,
|
||||||
|
Port: port,
|
||||||
|
IPs: ips,
|
||||||
|
TXT: txt,
|
||||||
|
serviceAddr: fmt.Sprintf("%s.%s.", trimDot(service), trimDot(domain)),
|
||||||
|
instanceAddr: fmt.Sprintf("%s.%s.%s.", instance, trimDot(service), trimDot(domain)),
|
||||||
|
enumAddr: fmt.Sprintf("_services._dns-sd._udp.%s.", trimDot(domain)),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// trimDot is used to trim the dots from the start or end of a string
|
||||||
|
func trimDot(s string) string {
|
||||||
|
return strings.Trim(s, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Records returns DNS records in response to a DNS question.
|
||||||
|
func (m *MDNSService) Records(q dns.Question) []dns.RR {
|
||||||
|
switch q.Name {
|
||||||
|
case m.enumAddr:
|
||||||
|
return m.serviceEnum(q)
|
||||||
|
case m.serviceAddr:
|
||||||
|
return m.serviceRecords(q)
|
||||||
|
case m.instanceAddr:
|
||||||
|
return m.instanceRecords(q)
|
||||||
|
case m.HostName:
|
||||||
|
if q.Qtype == dns.TypeA || q.Qtype == dns.TypeAAAA {
|
||||||
|
return m.instanceRecords(q)
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MDNSService) serviceEnum(q dns.Question) []dns.RR {
|
||||||
|
switch q.Qtype {
|
||||||
|
case dns.TypeANY:
|
||||||
|
fallthrough
|
||||||
|
case dns.TypePTR:
|
||||||
|
rr := &dns.PTR{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: q.Name,
|
||||||
|
Rrtype: dns.TypePTR,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: defaultTTL,
|
||||||
|
},
|
||||||
|
Ptr: m.serviceAddr,
|
||||||
|
}
|
||||||
|
return []dns.RR{rr}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceRecords is called when the query matches the service name
|
||||||
|
func (m *MDNSService) serviceRecords(q dns.Question) []dns.RR {
|
||||||
|
switch q.Qtype {
|
||||||
|
case dns.TypeANY:
|
||||||
|
fallthrough
|
||||||
|
case dns.TypePTR:
|
||||||
|
// Build a PTR response for the service
|
||||||
|
rr := &dns.PTR{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: q.Name,
|
||||||
|
Rrtype: dns.TypePTR,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: defaultTTL,
|
||||||
|
},
|
||||||
|
Ptr: m.instanceAddr,
|
||||||
|
}
|
||||||
|
servRec := []dns.RR{rr}
|
||||||
|
|
||||||
|
// Get the instance records
|
||||||
|
instRecs := m.instanceRecords(dns.Question{
|
||||||
|
Name: m.instanceAddr,
|
||||||
|
Qtype: dns.TypeANY,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return the service record with the instance records
|
||||||
|
return append(servRec, instRecs...)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceRecords is called when the query matches the instance name
|
||||||
|
func (m *MDNSService) instanceRecords(q dns.Question) []dns.RR {
|
||||||
|
switch q.Qtype {
|
||||||
|
case dns.TypeANY:
|
||||||
|
// Get the SRV, which includes A and AAAA
|
||||||
|
recs := m.instanceRecords(dns.Question{
|
||||||
|
Name: m.instanceAddr,
|
||||||
|
Qtype: dns.TypeSRV,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add the TXT record
|
||||||
|
recs = append(recs, m.instanceRecords(dns.Question{
|
||||||
|
Name: m.instanceAddr,
|
||||||
|
Qtype: dns.TypeTXT,
|
||||||
|
})...)
|
||||||
|
return recs
|
||||||
|
|
||||||
|
case dns.TypeA:
|
||||||
|
var rr []dns.RR
|
||||||
|
for _, ip := range m.IPs {
|
||||||
|
if ip4 := ip.To4(); ip4 != nil {
|
||||||
|
rr = append(rr, &dns.A{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: m.HostName,
|
||||||
|
Rrtype: dns.TypeA,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: defaultTTL,
|
||||||
|
},
|
||||||
|
A: ip4,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rr
|
||||||
|
|
||||||
|
case dns.TypeAAAA:
|
||||||
|
var rr []dns.RR
|
||||||
|
for _, ip := range m.IPs {
|
||||||
|
if ip.To4() != nil {
|
||||||
|
// TODO(reddaly): IPv4 addresses could be encoded in IPv6 format and
|
||||||
|
// putinto AAAA records, but the current logic puts ipv4-encodable
|
||||||
|
// addresses into the A records exclusively. Perhaps this should be
|
||||||
|
// configurable?
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip16 := ip.To16(); ip16 != nil {
|
||||||
|
rr = append(rr, &dns.AAAA{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: m.HostName,
|
||||||
|
Rrtype: dns.TypeAAAA,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: defaultTTL,
|
||||||
|
},
|
||||||
|
AAAA: ip16,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rr
|
||||||
|
|
||||||
|
case dns.TypeSRV:
|
||||||
|
// Create the SRV Record
|
||||||
|
srv := &dns.SRV{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: q.Name,
|
||||||
|
Rrtype: dns.TypeSRV,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: defaultTTL,
|
||||||
|
},
|
||||||
|
Priority: 10,
|
||||||
|
Weight: 1,
|
||||||
|
Port: uint16(m.Port),
|
||||||
|
Target: m.HostName,
|
||||||
|
}
|
||||||
|
recs := []dns.RR{srv}
|
||||||
|
|
||||||
|
// Add the A record
|
||||||
|
recs = append(recs, m.instanceRecords(dns.Question{
|
||||||
|
Name: m.instanceAddr,
|
||||||
|
Qtype: dns.TypeA,
|
||||||
|
})...)
|
||||||
|
|
||||||
|
// Add the AAAA record
|
||||||
|
recs = append(recs, m.instanceRecords(dns.Question{
|
||||||
|
Name: m.instanceAddr,
|
||||||
|
Qtype: dns.TypeAAAA,
|
||||||
|
})...)
|
||||||
|
return recs
|
||||||
|
|
||||||
|
case dns.TypeTXT:
|
||||||
|
txt := &dns.TXT{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: q.Name,
|
||||||
|
Rrtype: dns.TypeTXT,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: defaultTTL,
|
||||||
|
},
|
||||||
|
Txt: m.TXT,
|
||||||
|
}
|
||||||
|
return []dns.RR{txt}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue