mirror of
https://github.com/bettercap/bettercap
synced 2025-08-21 05:53:20 -07:00
misc: small fix or general refactoring i did not bother commenting
This commit is contained in:
parent
2966153adf
commit
26c532316a
21 changed files with 355 additions and 75 deletions
482
modules/zerogod/zeroconf/client.go
Normal file
482
modules/zerogod/zeroconf/client.go
Normal file
|
@ -0,0 +1,482 @@
|
|||
package zeroconf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff"
|
||||
"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 = 0x01
|
||||
IPv6 = 0x02
|
||||
IPv4AndIPv6 = (IPv4 | IPv6) //< Default option.
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Resolver acts as entry point for service lookups and to browse the DNS-SD.
|
||||
type Resolver struct {
|
||||
c *client
|
||||
}
|
||||
|
||||
// NewResolver creates a new resolver and joins the UDP multicast groups to
|
||||
// listen for mDNS messages.
|
||||
func NewResolver(options ...ClientOption) (*Resolver, error) {
|
||||
// Apply default configuration and load supplied options.
|
||||
var conf = clientOpts{
|
||||
listenOn: IPv4AndIPv6,
|
||||
}
|
||||
for _, o := range options {
|
||||
if o != nil {
|
||||
o(&conf)
|
||||
}
|
||||
}
|
||||
|
||||
c, err := newClient(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Resolver{
|
||||
c: c,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Browse for all services of a given type in a given domain.
|
||||
func (r *Resolver) Browse(ctx context.Context, service, domain string, entries chan<- *ServiceEntry) error {
|
||||
params := defaultParams(service)
|
||||
if domain != "" {
|
||||
params.SetDomain(domain)
|
||||
}
|
||||
params.Entries = entries
|
||||
params.isBrowsing = true
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
go r.c.mainloop(ctx, params)
|
||||
|
||||
err := r.c.query(params)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
// If previous probe was ok, it should be fine now. In case of an error later on,
|
||||
// the entries' queue is closed.
|
||||
go func() {
|
||||
if err := r.c.periodicQuery(ctx, params); err != nil {
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lookup a specific service by its name and type in a given domain.
|
||||
func (r *Resolver) Lookup(ctx context.Context, instance, service, domain string, entries chan<- *ServiceEntry) error {
|
||||
params := defaultParams(service)
|
||||
params.Instance = instance
|
||||
if domain != "" {
|
||||
params.SetDomain(domain)
|
||||
}
|
||||
params.Entries = entries
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
go r.c.mainloop(ctx, params)
|
||||
err := r.c.query(params)
|
||||
if err != nil {
|
||||
// cancel mainloop
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
// If previous probe was ok, it should be fine now. In case of an error later on,
|
||||
// the entries' queue is closed.
|
||||
go func() {
|
||||
if err := r.c.periodicQuery(ctx, params); err != nil {
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// defaultParams returns a default set of QueryParams.
|
||||
func defaultParams(service string) *lookupParams {
|
||||
return newLookupParams("", service, "local", false, make(chan *ServiceEntry))
|
||||
}
|
||||
|
||||
// Client structure encapsulates both IPv4/IPv6 UDP connections.
|
||||
type client struct {
|
||||
ipv4conn *ipv4.PacketConn
|
||||
ipv6conn *ipv6.PacketConn
|
||||
ifaces []net.Interface
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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, sentEntries map[string]*ServiceEntry
|
||||
sentEntries = make(map[string]*ServiceEntry)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Context expired. Notify subscriber that we are done here.
|
||||
params.done()
|
||||
c.shutdown()
|
||||
return
|
||||
case msg := <-msgCh:
|
||||
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].TTL = rr.Hdr.Ttl
|
||||
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].TTL = rr.Hdr.Ttl
|
||||
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].TTL = rr.Hdr.Ttl
|
||||
}
|
||||
}
|
||||
// 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.TTL == 0 {
|
||||
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 {
|
||||
bo := backoff.NewExponentialBackOff()
|
||||
bo.InitialInterval = 4 * time.Second
|
||||
bo.MaxInterval = 60 * time.Second
|
||||
bo.MaxElapsedTime = 0
|
||||
bo.Reset()
|
||||
|
||||
var timer *time.Timer
|
||||
defer func() {
|
||||
if timer != nil {
|
||||
timer.Stop()
|
||||
}
|
||||
}()
|
||||
for {
|
||||
// Backoff and cancel logic.
|
||||
wait := bo.NextBackOff()
|
||||
if wait == backoff.Stop {
|
||||
return fmt.Errorf("periodicQuery: abort due to timeout")
|
||||
}
|
||||
if timer == nil {
|
||||
timer = time.NewTimer(wait)
|
||||
} else {
|
||||
timer.Reset(wait)
|
||||
}
|
||||
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():
|
||||
return ctx.Err()
|
||||
}
|
||||
// Do periodic query.
|
||||
if err := c.query(params); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
if err := c.sendQuery(m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
117
modules/zerogod/zeroconf/connection.go
Normal file
117
modules/zerogod/zeroconf/connection.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
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)
|
||||
_ = pkConn.SetMulticastHopLimit(255)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
_ = pkConn.SetMulticastTTL(255)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
modules/zerogod/zeroconf/doc.go
Normal file
14
modules/zerogod/zeroconf/doc.go
Normal 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
|
796
modules/zerogod/zeroconf/server.go
Normal file
796
modules/zerogod/zeroconf/server.go
Normal file
|
@ -0,0 +1,796 @@
|
|||
package zeroconf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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
|
||||
)
|
||||
|
||||
// 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) (*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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.service = entry
|
||||
go s.mainloop()
|
||||
go s.probe()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// RegisterExternalResponder registers a service proxy. This call will skip the hostname/IP lookup and
|
||||
// will use the provided values.
|
||||
func RegisterExternalResponder(instance, service, domain string, port int, host string, ips []string, text []string, ifaces []net.Interface) (*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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.service = entry
|
||||
go s.mainloop()
|
||||
go s.probe()
|
||||
|
||||
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
|
||||
shutdownEnd sync.WaitGroup
|
||||
isShutdown bool
|
||||
ttl uint32
|
||||
}
|
||||
|
||||
// Constructs server structure
|
||||
func newServer(ifaces []net.Interface) (*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: 3200,
|
||||
shouldShutdown: make(chan struct{}),
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Start listeners and waits for the shutdown signal from exit channel
|
||||
func (s *Server) mainloop() {
|
||||
if s.ipv4conn != nil {
|
||||
go s.recv4(s.ipv4conn)
|
||||
}
|
||||
if s.ipv6conn != nil {
|
||||
go s.recv6(s.ipv6conn)
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown closes all udp connections and unregisters the service
|
||||
func (s *Server) Shutdown() {
|
||||
s.shutdown()
|
||||
}
|
||||
|
||||
// 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
|
||||
func (s *Server) TTL(ttl uint32) {
|
||||
s.ttl = ttl
|
||||
}
|
||||
|
||||
// Shutdown server will close currently open connections & channel
|
||||
func (s *Server) shutdown() error {
|
||||
s.shutdownLock.Lock()
|
||||
defer s.shutdownLock.Unlock()
|
||||
if s.isShutdown {
|
||||
return errors.New("server is already shutdown")
|
||||
}
|
||||
|
||||
err := s.unregister()
|
||||
|
||||
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.shutdownEnd.Wait()
|
||||
s.isShutdown = true
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// recv is a long running routine to receive packets from an interface
|
||||
func (s *Server) recv4(c *ipv4.PacketConn) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 65536)
|
||||
s.shutdownEnd.Add(1)
|
||||
defer s.shutdownEnd.Done()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// recv is a long running routine to receive packets from an interface
|
||||
func (s *Server) recv6(c *ipv6.PacketConn) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 65536)
|
||||
s.shutdownEnd.Add(1)
|
||||
defer s.shutdownEnd.Done()
|
||||
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() {
|
||||
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}
|
||||
|
||||
randomizer := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
for i := 0; i < multicastRepetitions; i++ {
|
||||
if err := s.multicastResponse(q, 0); err != nil {
|
||||
log.Println("[ERR] zeroconf: failed to send probe:", err.Error())
|
||||
}
|
||||
time.Sleep(time.Duration(randomizer.Intn(250)) * time.Millisecond)
|
||||
}
|
||||
|
||||
// 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 := 1 * 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())
|
||||
}
|
||||
}
|
||||
time.Sleep(timeout)
|
||||
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 us 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("error creating multicast response: %v", 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
|
||||
}
|
125
modules/zerogod/zeroconf/service.go
Normal file
125
modules/zerogod/zeroconf/service.go
Normal file
|
@ -0,0 +1,125 @@
|
|||
package zeroconf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (s *ServiceRecord) SetDomain(domain string) {
|
||||
s.Domain = domain
|
||||
|
||||
s.serviceName = fmt.Sprintf("%s.%s.", trimDot(s.Service), trimDot(domain))
|
||||
if s.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)
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
s.SetDomain(domain)
|
||||
|
||||
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
|
||||
TTL uint32 `json:"ttl"` // TTL of the service record
|
||||
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
modules/zerogod/zeroconf/utils.go
Normal file
13
modules/zerogod/zeroconf/utils.go
Normal 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, ".")
|
||||
}
|
|
@ -3,10 +3,14 @@ package zerogod
|
|||
import (
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
"github.com/bettercap/bettercap/v2/tls"
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/pcap"
|
||||
)
|
||||
|
||||
type ZeroGod struct {
|
||||
session.SessionModule
|
||||
sniffer *pcap.Handle
|
||||
snifferCh chan gopacket.Packet
|
||||
browser *Browser
|
||||
advertiser *Advertiser
|
||||
}
|
||||
|
@ -14,8 +18,6 @@ type ZeroGod struct {
|
|||
func NewZeroGod(s *session.Session) *ZeroGod {
|
||||
mod := &ZeroGod{
|
||||
SessionModule: session.NewSessionModule("zerogod", s),
|
||||
browser: nil,
|
||||
advertiser: nil,
|
||||
}
|
||||
|
||||
mod.SessionModule.Requires("net.recon")
|
||||
|
@ -115,12 +117,6 @@ func (mod *ZeroGod) Configure() (err error) {
|
|||
return session.ErrAlreadyStarted(mod.Name())
|
||||
}
|
||||
|
||||
if mod.browser != nil {
|
||||
mod.browser.Stop(false)
|
||||
}
|
||||
|
||||
mod.browser = NewBrowser()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -130,7 +126,7 @@ func (mod *ZeroGod) Start() (err error) {
|
|||
}
|
||||
|
||||
// start the root discovery
|
||||
if err = mod.startResolver(DNSSD_DISCOVERY_SERVICE); err != nil {
|
||||
if err = mod.startDiscovery(DNSSD_DISCOVERY_SERVICE); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -144,14 +140,6 @@ func (mod *ZeroGod) Start() (err error) {
|
|||
func (mod *ZeroGod) Stop() error {
|
||||
return mod.SetRunning(false, func() {
|
||||
mod.stopAdvertiser()
|
||||
if mod.browser != nil {
|
||||
mod.Debug("stopping discovery")
|
||||
|
||||
mod.browser.Stop(true)
|
||||
|
||||
mod.Debug("stopped")
|
||||
|
||||
mod.browser = nil
|
||||
}
|
||||
mod.stopDiscovery()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/evilsocket/islazy/ops"
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
)
|
||||
|
||||
|
@ -30,10 +31,12 @@ var TCP_HANDLERS = map[string]Handler{
|
|||
type Acceptor struct {
|
||||
mod *ZeroGod
|
||||
srvHost string
|
||||
proto string
|
||||
port uint16
|
||||
service string
|
||||
tlsConfig *tls.Config
|
||||
listener net.Listener
|
||||
tcpListener net.Listener
|
||||
udpListener *net.UDPConn
|
||||
running bool
|
||||
context context.Context
|
||||
ctxCancel context.CancelFunc
|
||||
|
@ -53,9 +56,12 @@ type HandlerContext struct {
|
|||
|
||||
func NewAcceptor(mod *ZeroGod, service string, srvHost string, port uint16, tlsConfig *tls.Config, ippAttributes map[string]string) *Acceptor {
|
||||
context, ctcCancel := context.WithCancel(context.Background())
|
||||
proto := ops.Ternary(strings.Contains(service, "_tcp"), "tcp", "udp").(string)
|
||||
|
||||
acceptor := Acceptor{
|
||||
mod: mod,
|
||||
port: port,
|
||||
proto: proto,
|
||||
service: service,
|
||||
context: context,
|
||||
ctxCancel: ctcCancel,
|
||||
|
@ -72,36 +78,34 @@ func NewAcceptor(mod *ZeroGod, service string, srvHost string, port uint16, tlsC
|
|||
}
|
||||
|
||||
if acceptor.handler.Handle == nil {
|
||||
mod.Warning("no protocol handler found for service %s, using generic dump handler", tui.Yellow(service))
|
||||
mod.Warning("no protocol handler found for service %s, using generic %s dump handler", tui.Yellow(service), proto)
|
||||
acceptor.handler.Handle = handleGenericTCP
|
||||
} else {
|
||||
mod.Info("found %s protocol handler", tui.Green(service))
|
||||
mod.Info("found %s %s protocol handler", proto, tui.Green(service))
|
||||
}
|
||||
|
||||
return &acceptor
|
||||
}
|
||||
|
||||
func (a *Acceptor) Start() (err error) {
|
||||
func (a *Acceptor) startTCP() (err error) {
|
||||
var lc net.ListenConfig
|
||||
|
||||
if a.listener, err = lc.Listen(a.context, "tcp", fmt.Sprintf("0.0.0.0:%d", a.port)); err != nil {
|
||||
if a.tcpListener, err = lc.Listen(a.context, "tcp", fmt.Sprintf("0.0.0.0:%d", a.port)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a.tlsConfig != nil {
|
||||
a.listener = tls.NewListener(a.listener, a.tlsConfig)
|
||||
a.tcpListener = tls.NewListener(a.tcpListener, a.tlsConfig)
|
||||
}
|
||||
|
||||
a.running = true
|
||||
go func() {
|
||||
a.mod.Debug("tcp listener for port %d (%s) started", a.port, tui.Green(a.service))
|
||||
a.mod.Debug("%s listener for port %d (%s) started", a.proto, a.port, tui.Green(a.service))
|
||||
for a.running {
|
||||
if conn, err := a.listener.Accept(); err != nil {
|
||||
if conn, err := a.tcpListener.Accept(); err != nil {
|
||||
if a.running {
|
||||
a.mod.Error("%v", err)
|
||||
}
|
||||
} else {
|
||||
a.mod.Debug("accepted connection for service %s (port %d): %v", tui.Green(a.service), a.port, conn.RemoteAddr())
|
||||
a.mod.Debug("accepted %s connection for service %s (port %d): %v", a.proto, tui.Green(a.service), a.port, conn.RemoteAddr())
|
||||
go a.handler.Handle(&HandlerContext{
|
||||
service: a.service,
|
||||
mod: a.mod,
|
||||
|
@ -113,17 +117,60 @@ func (a *Acceptor) Start() (err error) {
|
|||
})
|
||||
}
|
||||
}
|
||||
a.mod.Debug("tcp listener for port %d (%s) stopped", a.port, tui.Green(a.service))
|
||||
a.mod.Debug("%s listener for port %d (%s) stopped", a.proto, a.port, tui.Green(a.service))
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Acceptor) Stop() {
|
||||
a.mod.Debug("stopping tcp listener for port %d", a.port)
|
||||
a.running = false
|
||||
a.ctxCancel()
|
||||
<-a.context.Done()
|
||||
a.listener.Close()
|
||||
a.mod.Debug("tcp listener for port %d stopped", a.port)
|
||||
func (a *Acceptor) startUDP() (err error) {
|
||||
if udpAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("0.0.0.0:%d", a.port)); err != nil {
|
||||
return err
|
||||
} else if a.udpListener, err = net.ListenUDP("udp", udpAddr); err != nil {
|
||||
return err
|
||||
} else {
|
||||
a.running = true
|
||||
go func() {
|
||||
var buffer [4096]byte
|
||||
|
||||
a.mod.Info("%s listener for port %d (%s) started", a.proto, a.port, tui.Green(a.service))
|
||||
|
||||
for a.running {
|
||||
if n, addr, err := a.udpListener.ReadFromUDP(buffer[0:]); err != nil {
|
||||
a.mod.Warning("error reading udp packet: %v", err)
|
||||
} else if n <= 0 {
|
||||
a.mod.Info("empty read")
|
||||
} else {
|
||||
a.mod.Info("%v:\n%s", addr, Dump(buffer[0:n]))
|
||||
}
|
||||
}
|
||||
|
||||
a.mod.Info("%s listener for port %d (%s) stopped", a.proto, a.port, tui.Green(a.service))
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Acceptor) Start() (err error) {
|
||||
if a.proto == "tcp" {
|
||||
return a.startTCP()
|
||||
} else {
|
||||
return a.startUDP()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Acceptor) Stop() {
|
||||
a.mod.Debug("stopping %s listener for port %d", a.proto, a.port)
|
||||
a.running = false
|
||||
|
||||
if a.proto == "tcp" {
|
||||
a.ctxCancel()
|
||||
<-a.context.Done()
|
||||
a.tcpListener.Close()
|
||||
} else {
|
||||
a.udpListener.Close()
|
||||
}
|
||||
|
||||
a.mod.Debug("%s listener for port %d stopped", a.proto, a.port)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -20,6 +22,27 @@ type Advertiser struct {
|
|||
Acceptors []*Acceptor
|
||||
}
|
||||
|
||||
func isPortAvailable(port int) bool {
|
||||
address := fmt.Sprintf("127.0.0.1:%d", port)
|
||||
if conn, err := net.DialTimeout("tcp", address, 10*time.Millisecond); err != nil {
|
||||
return true
|
||||
} else if conn == nil {
|
||||
return true
|
||||
} else {
|
||||
conn.Close()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isPortRequested(svc *ServiceData, services []*ServiceData) bool {
|
||||
for _, other := range services {
|
||||
if svc != other && svc.Port == other.Port {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (mod *ZeroGod) loadTLSConfig() (*tls.Config, error) {
|
||||
var certFile string
|
||||
var keyFile string
|
||||
|
@ -104,6 +127,21 @@ func (mod *ZeroGod) startAdvertiser(fileName string) error {
|
|||
Acceptors: make([]*Acceptor, 0),
|
||||
}
|
||||
|
||||
// fix ports
|
||||
for _, svc := range advertiser.Services {
|
||||
// if no external responder has been specified, check if port is available
|
||||
if svc.Responder == "" {
|
||||
for svc.Port == 0 || !isPortAvailable(svc.Port) || isPortRequested(svc, services) {
|
||||
newPort := (rand.Intn(65535-1024) + 1024)
|
||||
mod.Warning("port %d for service %s is not avaialble, trying %d ...",
|
||||
svc.Port,
|
||||
svc.FullName(),
|
||||
newPort)
|
||||
svc.Port = newPort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// paralleize initialization
|
||||
svcChan := make(chan error, numServices)
|
||||
for _, svc := range advertiser.Services {
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"context"
|
||||
"sort"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/zeroconf"
|
||||
"github.com/bettercap/bettercap/v2/modules/zerogod/zeroconf"
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,19 +1,31 @@
|
|||
package zerogod
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/modules/zerogod/zeroconf"
|
||||
"github.com/bettercap/bettercap/v2/network"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
"github.com/bettercap/bettercap/v2/zeroconf"
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
|
||||
// a service has been discovered
|
||||
type ServiceDiscoveryEvent struct {
|
||||
Service zeroconf.ServiceEntry `json:"service"`
|
||||
Endpoint *network.Endpoint `json:"endpoint"`
|
||||
}
|
||||
|
||||
// an endpoint is browsing for specific services
|
||||
type BrowsingEvent struct {
|
||||
Source string `json:"source"`
|
||||
Query layers.DNS `json:"service"`
|
||||
Endpoint *network.Endpoint `json:"endpoint"`
|
||||
}
|
||||
|
||||
func (mod *ZeroGod) onServiceDiscovered(svc *zeroconf.ServiceEntry) {
|
||||
mod.Debug("%++v", *svc)
|
||||
|
||||
|
@ -21,8 +33,15 @@ func (mod *ZeroGod) onServiceDiscovered(svc *zeroconf.ServiceEntry) {
|
|||
svcName := strings.Replace(svc.Instance, ".local", "", 1)
|
||||
if !mod.browser.HasResolverFor(svcName) {
|
||||
mod.Debug("discovered service %s", tui.Green(svcName))
|
||||
if err := mod.startResolver(svcName); err != nil {
|
||||
if ch, err := mod.browser.StartBrowsing(svcName, "local.", mod); err != nil {
|
||||
mod.Error("%v", err)
|
||||
} else {
|
||||
// start listening on this channel
|
||||
go func() {
|
||||
for entry := range ch {
|
||||
mod.onServiceDiscovered(entry)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
return
|
||||
|
@ -58,17 +77,116 @@ func (mod *ZeroGod) onServiceDiscovered(svc *zeroconf.ServiceEntry) {
|
|||
mod.Debug("got mdns entry for unknown ip: %++v", *svc)
|
||||
}
|
||||
|
||||
session.I.Events.Add("mdns.service", event)
|
||||
session.I.Events.Add("zeroconf.service", event)
|
||||
session.I.Refresh()
|
||||
}
|
||||
|
||||
func (mod *ZeroGod) startResolver(service string) error {
|
||||
func (mod *ZeroGod) onPacket(pkt gopacket.Packet) {
|
||||
mod.Debug("%++v", pkt)
|
||||
|
||||
netLayer := pkt.NetworkLayer()
|
||||
if netLayer == nil {
|
||||
mod.Warning("not network layer in packet %+v", pkt)
|
||||
return
|
||||
}
|
||||
var srcIP net.IP
|
||||
// var dstIP net.IP
|
||||
switch netLayer.LayerType() {
|
||||
case layers.LayerTypeIPv4:
|
||||
ip := netLayer.(*layers.IPv4)
|
||||
srcIP = ip.SrcIP
|
||||
// dstIP = ip.DstIP
|
||||
case layers.LayerTypeIPv6:
|
||||
ip := netLayer.(*layers.IPv6)
|
||||
srcIP = ip.SrcIP
|
||||
// dstIP = ip.DstIP
|
||||
default:
|
||||
mod.Warning("unexpected network layer type %v in packet %+v", netLayer.LayerType(), pkt)
|
||||
return
|
||||
}
|
||||
|
||||
// not interested in packet generated by us
|
||||
if srcIP.Equal(mod.Session.Interface.IP) || srcIP.Equal(mod.Session.Interface.IPv6) {
|
||||
mod.Debug("skipping local packet")
|
||||
return
|
||||
}
|
||||
|
||||
udp := pkt.Layer(layers.LayerTypeUDP)
|
||||
if udp == nil {
|
||||
mod.Warning("not udp layer in packet %+v", pkt)
|
||||
return
|
||||
}
|
||||
|
||||
dns := layers.DNS{}
|
||||
if err := dns.DecodeFromBytes(udp.LayerPayload(), gopacket.NilDecodeFeedback); err != nil {
|
||||
mod.Warning("could not decode DNS (%v) in packet %+v", err, pkt)
|
||||
return
|
||||
}
|
||||
|
||||
// since the browser is already checking for these, we are only interested in queries
|
||||
numQs := len(dns.Questions)
|
||||
if numQs == 0 {
|
||||
mod.Debug("skipping answers only packet")
|
||||
return
|
||||
}
|
||||
|
||||
event := BrowsingEvent{
|
||||
Source: srcIP.String(),
|
||||
Query: dns,
|
||||
Endpoint: mod.Session.Lan.GetByIp(srcIP.String()),
|
||||
}
|
||||
|
||||
if event.Endpoint == nil {
|
||||
// TODO: if nil, this is probably an IPv6 only record, try to somehow check which known IPv4 it is
|
||||
// TODO: make configurable?
|
||||
mod.Debug("got mdns packet from unknown ip %s: %++v", srcIP, dns)
|
||||
return
|
||||
}
|
||||
|
||||
session.I.Events.Add("zeroconf.browsing", event)
|
||||
session.I.Refresh()
|
||||
}
|
||||
|
||||
func (mod *ZeroGod) startDiscovery(service string) (err error) {
|
||||
mod.Debug("starting resolver for service %s", tui.Yellow(service))
|
||||
|
||||
// create passive sniffer
|
||||
if mod.sniffer != nil {
|
||||
mod.sniffer.Close()
|
||||
}
|
||||
|
||||
readTimeout := 500 * time.Millisecond
|
||||
if mod.sniffer, err = network.CaptureWithTimeout(mod.Session.Interface.Name(), readTimeout); err != nil {
|
||||
return err
|
||||
} else if err = mod.sniffer.SetBPFFilter("udp and port 5353"); err != nil {
|
||||
return err
|
||||
}
|
||||
// prepare source and start listening for packets
|
||||
src := gopacket.NewPacketSource(mod.sniffer, mod.sniffer.LinkType())
|
||||
mod.snifferCh = src.Packets()
|
||||
// start listening for new packets
|
||||
go func() {
|
||||
mod.Debug("sniffer started")
|
||||
for pkt := range mod.snifferCh {
|
||||
if !mod.Running() {
|
||||
mod.Debug("end pkt loop (pkt=%v)", pkt)
|
||||
break
|
||||
}
|
||||
mod.onPacket(pkt)
|
||||
}
|
||||
mod.Debug("sniffer stopped")
|
||||
}()
|
||||
|
||||
// create service browser
|
||||
if mod.browser != nil {
|
||||
mod.browser.Stop(false)
|
||||
}
|
||||
mod.browser = NewBrowser()
|
||||
// start active browsing
|
||||
if ch, err := mod.browser.StartBrowsing(service, "local.", mod); err != nil {
|
||||
return err
|
||||
} else {
|
||||
// start listening
|
||||
// start listening for new services
|
||||
go func() {
|
||||
for entry := range ch {
|
||||
mod.onServiceDiscovered(entry)
|
||||
|
@ -78,3 +196,21 @@ func (mod *ZeroGod) startResolver(service string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mod *ZeroGod) stopDiscovery() {
|
||||
if mod.browser != nil {
|
||||
mod.Debug("stopping discovery")
|
||||
mod.browser.Stop(true)
|
||||
mod.browser = nil
|
||||
mod.Debug("discovery stopped")
|
||||
}
|
||||
|
||||
if mod.sniffer != nil {
|
||||
mod.Debug("stopping sniffer")
|
||||
mod.snifferCh <- nil
|
||||
mod.sniffer.Close()
|
||||
mod.sniffer = nil
|
||||
mod.snifferCh = nil
|
||||
mod.Debug("sniffer stopped")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/modules/syn_scan"
|
||||
"github.com/bettercap/bettercap/v2/modules/zerogod/zeroconf"
|
||||
"github.com/bettercap/bettercap/v2/network"
|
||||
"github.com/bettercap/bettercap/v2/zeroconf"
|
||||
"github.com/evilsocket/islazy/str"
|
||||
)
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ func ippClientHandler(ctx *HandlerContext) {
|
|||
read, err := ctx.client.Read(buf)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
ctx.mod.Debug("EOF, client %s disconnected", clientIP)
|
||||
return
|
||||
}
|
||||
ctx.mod.Warning("error while reading from %v: %v", clientIP, err)
|
||||
|
@ -67,12 +68,12 @@ func ippClientHandler(ctx *HandlerContext) {
|
|||
reader := bufio.NewReader(bytes.NewReader(raw_req))
|
||||
http_req, err := http.ReadRequest(reader)
|
||||
if err != nil {
|
||||
ctx.mod.Error("error while parsing http request from %v: %v", clientIP, err)
|
||||
ctx.mod.Error("error while parsing http request from %v: %v\n%s", clientIP, err, Dump(raw_req))
|
||||
return
|
||||
}
|
||||
|
||||
clientUA := http_req.UserAgent()
|
||||
ctx.mod.Debug("%v -> %s", clientIP, tui.Green(clientUA))
|
||||
ctx.mod.Info("%v -> %s", clientIP, tui.Green(clientUA))
|
||||
|
||||
ipp_body, err := ippReadRequestBody(ctx, http_req)
|
||||
if err != nil {
|
||||
|
@ -92,8 +93,14 @@ func ippClientHandler(ctx *HandlerContext) {
|
|||
ipp_op_name = name
|
||||
}
|
||||
|
||||
ctx.mod.Info("%s <- %s (%s) %s",
|
||||
reqUsername := tui.Dim("<unknown>")
|
||||
if value, found := ipp_req.OperationAttributes["requesting-user-name"]; found {
|
||||
reqUsername = tui.Blue(value.(string))
|
||||
}
|
||||
|
||||
ctx.mod.Info("%s <- %s@%s (%s) %s",
|
||||
tui.Yellow(ctx.service),
|
||||
reqUsername,
|
||||
clientIP,
|
||||
tui.Green(clientUA),
|
||||
tui.Bold(ipp_op_name))
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/zeroconf"
|
||||
"github.com/bettercap/bettercap/v2/modules/zerogod/zeroconf"
|
||||
"github.com/evilsocket/islazy/str"
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/zeroconf"
|
||||
"github.com/bettercap/bettercap/v2/modules/zerogod/zeroconf"
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
)
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/evilsocket/islazy/ops"
|
||||
"github.com/evilsocket/islazy/str"
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
)
|
||||
|
@ -20,7 +21,10 @@ func (mod *ZeroGod) show(filter string, withData bool) error {
|
|||
|
||||
for _, entry := range entries {
|
||||
if endpoint := mod.Session.Lan.GetByIp(entry.Address); endpoint != nil {
|
||||
fmt.Fprintf(mod.Session.Events.Stdout, "* %s (%s)\n", tui.Bold(endpoint.IpAddress), tui.Dim(endpoint.Vendor))
|
||||
fmt.Fprintf(mod.Session.Events.Stdout, "* %s (%s)%s\n",
|
||||
tui.Bold(endpoint.IpAddress),
|
||||
tui.Dim(endpoint.Vendor),
|
||||
ops.Ternary(endpoint.Hostname == "", "", " "+tui.Bold(endpoint.Hostname)))
|
||||
} else {
|
||||
fmt.Fprintf(mod.Session.Events.Stdout, "* %s\n", tui.Bold(entry.Address))
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue