This commit is contained in:
Simone Margaritelli 2024-09-18 23:21:30 +02:00
commit 67cc9680ed
17 changed files with 1833 additions and 274 deletions

2
go.mod
View file

@ -23,7 +23,6 @@ require (
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/go-bexpr v0.1.14
github.com/hashicorp/mdns v1.0.5
github.com/inconshreveable/go-vhost v1.0.0
github.com/jpillora/go-tld v1.2.1
github.com/malfunkt/iprange v0.9.0
@ -36,6 +35,7 @@ require (
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64
go.einride.tech/can v0.12.0
golang.org/x/net v0.28.0
gopkg.in/yaml.v3 v3.0.1
)
require (

9
go.sum
View file

@ -55,8 +55,6 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-bexpr v0.1.14 h1:uKDeyuOhWhT1r5CiMTjdVY4Aoxdxs6EtwgTGnlosyp4=
github.com/hashicorp/go-bexpr v0.1.14/go.mod h1:gN7hRKB3s7yT+YvTdnhZVLTENejvhlkZ8UE4YVBS+Q8=
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/inconshreveable/go-vhost v1.0.0 h1:IK4VZTlXL4l9vz2IZoiSFbYaaqUW7dXJAiPriUN5Ur8=
github.com/inconshreveable/go-vhost v1.0.0/go.mod h1:aA6DnFhALT3zH0y+A39we+zbrdMC2N0X/q21e6FI0LU=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
@ -65,9 +63,11 @@ github.com/jpillora/go-tld v1.2.1 h1:kDKOkmXLlskqjcvNs7w5XHLep7c8WM7Xd4HQjxllVMk
github.com/jpillora/go-tld v1.2.1/go.mod h1:plzIl7xr5UWKGy7R+giuv+L/nOjrPjsoWxy/ST9OBUk=
github.com/kr/binarydist v0.1.0 h1:6kAoLA9FMMnNGSehX0s1PdjbEaACznAv/W219j2uvyo=
github.com/kr/binarydist v0.1.0/go.mod h1:DY7S//GCoz1BCd0B0EVrinCKAZN3pXe+MDaIZbXQVgM=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/malfunkt/iprange v0.9.0 h1:VCs0PKLUPotNVQTpVNszsut4lP7OCGNBwX+lOYBrnVQ=
github.com/malfunkt/iprange v0.9.0/go.mod h1:TRGqO/f95gh3LOndUGTL46+W0GXA91WTqyZ0Quwvt4U=
@ -88,7 +88,6 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQ
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab h1:n8cgpHzJ5+EDyDri2s/GC7a9+qK3/YEGnBsd0uS/8PY=
github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab/go.mod h1:y1pL58r5z2VvAjeG1VLGc8zOQgSOzbKN7kMHPvFXJ+8=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@ -134,9 +133,7 @@ golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190310074541-c10a0554eabf/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
@ -150,7 +147,6 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -180,6 +176,7 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=

View file

@ -134,6 +134,8 @@ func (mod *EventsStream) Render(output io.Writer, e session.Event) {
mod.viewUpdateEvent(output, e)
} else if e.Tag == "gateway.change" {
mod.viewGatewayEvent(output, e)
} else if e.Tag == "mdns.service" {
mod.viewMDNSEvent(output, e)
} else if e.Tag != "tick" && e.Tag != "session.started" && e.Tag != "session.stopped" {
fmt.Fprintf(output, "[%s] [%s] %v\n", e.Time.Format(mod.timeFormat), tui.Green(e.Tag), e)
}

View file

@ -0,0 +1,23 @@
package events_stream
import (
"fmt"
"io"
"github.com/bettercap/bettercap/v2/modules/mdns"
"github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/tui"
)
func (mod *EventsStream) viewMDNSEvent(output io.Writer, e session.Event) {
event := e.Data.(mdns.ServiceDiscoveryEvent)
fmt.Fprintf(output, "[%s] [%s] service %s detected for %s (%s):%d : %s\n",
e.Time.Format(mod.timeFormat),
tui.Green(e.Tag),
tui.Bold(event.Service.Name),
event.Service.AddrV4.String(),
tui.Dim(event.Service.Host),
event.Service.Port,
event.Service.Info,
)
}

475
modules/mdns/client.go Normal file
View file

@ -0,0 +1,475 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MIT
package mdns
import (
"context"
"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 // @Deprecated
AddrV6IPAddr *net.IPAddr
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 {
Module *MDNSModule
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
DisableIPv4 bool // Whether to disable usage of IPv4 for MDNS operations. Does not affect discovered addresses.
DisableIPv6 bool // Whether to disable usage of IPv6 for MDNS operations. Does not affect discovered addresses.
Logger *log.Logger // Optionally provide a *log.Logger to better manage log output.
}
// 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.
DisableIPv4: false,
DisableIPv6: false,
}
}
// 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 {
return QueryContext(context.Background(), params)
}
// QueryContext 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. QueryContext will attempt to stop the query
// on cancellation.
func QueryContext(ctx context.Context, params *QueryParam) error {
if params.Logger == nil {
params.Logger = log.Default()
}
// Create a new client
client, err := newClient(!params.DisableIPv4, !params.DisableIPv6, params.Logger)
if err != nil {
return err
}
defer client.Close()
go func() {
select {
case <-ctx.Done():
client.Close()
case <-client.closedCh:
return
}
}()
// 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 {
use_ipv4 bool
use_ipv6 bool
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.
log *log.Logger
}
// NewClient creates a new mdns Client that can be used to query
// for records
func newClient(v4 bool, v6 bool, logger *log.Logger) (*client, error) {
if !v4 && !v6 {
return nil, fmt.Errorf("Must enable at least one of IPv4 and IPv6 querying")
}
// TODO(reddaly): At least attempt to bind to the port required in the spec.
// Create a IPv4 listener
var uconn4 *net.UDPConn
var uconn6 *net.UDPConn
var mconn4 *net.UDPConn
var mconn6 *net.UDPConn
var err error
// Establish unicast connections
if v4 {
uconn4, err = net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
if err != nil {
logger.Printf("[ERR] mdns: Failed to bind to udp4 port: %v", err)
}
}
if v6 {
uconn6, err = net.ListenUDP("udp6", &net.UDPAddr{IP: net.IPv6zero, Port: 0})
if err != nil {
logger.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")
}
// Establish multicast connections
if v4 {
mconn4, err = net.ListenMulticastUDP("udp4", nil, ipv4Addr)
if err != nil {
logger.Printf("[ERR] mdns: Failed to bind to udp4 port: %v", err)
}
}
if v6 {
mconn6, err = net.ListenMulticastUDP("udp6", nil, ipv6Addr)
if err != nil {
logger.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")
}
// Check that unicast and multicast connections have been made for IPv4 and IPv6
// and disable the respective protocol if not.
if uconn4 == nil || mconn4 == nil {
logger.Printf("[INFO] mdns: Failed to listen to both unicast and multicast on IPv4")
uconn4 = nil
mconn4 = nil
v4 = false
}
if uconn6 == nil || mconn6 == nil {
logger.Printf("[INFO] mdns: Failed to listen to both unicast and multicast on IPv6")
uconn6 = nil
mconn6 = nil
v6 = false
}
if !v4 && !v6 {
return nil, fmt.Errorf("at least one of IPv4 and IPv6 must be enabled for querying")
}
c := &client{
use_ipv4: v4,
use_ipv6: v6,
ipv4MulticastConn: mconn4,
ipv6MulticastConn: mconn6,
ipv4UnicastConn: uconn4,
ipv6UnicastConn: uconn6,
closedCh: make(chan struct{}),
log: logger,
}
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
}
c.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 system
// default if not provided
func (c *client) setInterface(iface *net.Interface) error {
if c.use_ipv4 {
p := ipv4.NewPacketConn(c.ipv4UnicastConn)
if err := p.SetMulticastInterface(iface); err != nil {
return err
}
p = ipv4.NewPacketConn(c.ipv4MulticastConn)
if err := p.SetMulticastInterface(iface); err != nil {
return err
}
}
if c.use_ipv6 {
p2 := ipv6.NewPacketConn(c.ipv6UnicastConn)
if err := p2.SetMulticastInterface(iface); err != nil {
return err
}
p2 = ipv6.NewPacketConn(c.ipv6MulticastConn)
if err := p2.SetMulticastInterface(iface); err != nil {
return err
}
}
return nil
}
// msgAddr carries the message and source address from recv to message processing.
type msgAddr struct {
msg *dns.Msg
src *net.UDPAddr
}
// 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 *msgAddr, 32)
if c.use_ipv4 {
go c.recv(c.ipv4UnicastConn, msgCh)
go c.recv(c.ipv4MulticastConn, msgCh)
}
if c.use_ipv6 {
go c.recv(c.ipv6UnicastConn, 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 = true
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.msg.Answer, resp.msg.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 // @Deprecated
inp.AddrV6IPAddr = &net.IPAddr{IP: rr.AAAA}
// link-local IPv6 addresses must be qualified with a zone (interface). Zone is
// specific to this machine/network-namespace and so won't be carried in the
// mDNS message itself. We borrow the zone from the source address of the UDP
// packet, as the link-local address should be valid on that interface.
if rr.AAAA.IsLinkLocalUnicast() || rr.AAAA.IsLinkLocalMulticast() {
inp.AddrV6IPAddr.Zone = resp.src.Zone
}
}
if inp == nil {
params.Module.Debug("no inp for %v", answer)
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
params.Module.Debug("sending query for service %s", inp.Name)
m := new(dns.Msg)
m.SetQuestion(inp.Name, dns.TypePTR)
m.RecursionDesired = true
if err := c.sendQuery(m); err != nil {
params.Module.Error("failed to query instance %s: %v", inp.Name, err)
}
time.Sleep(time.Duration(1) * time.Millisecond)
}
}
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 {
_, err = c.ipv4UnicastConn.WriteToUDP(buf, ipv4Addr)
if err != nil {
return err
}
}
if c.ipv6UnicastConn != nil {
_, err = c.ipv6UnicastConn.WriteToUDP(buf, ipv6Addr)
if err != nil {
return err
}
}
return nil
}
// recv is used to receive until we get a shutdown
func (c *client) recv(l *net.UDPConn, msgCh chan *msgAddr) {
if l == nil {
return
}
buf := make([]byte, 65536)
for atomic.LoadInt32(&c.closed) == 0 {
n, addr, err := l.ReadFromUDP(buf)
if atomic.LoadInt32(&c.closed) == 1 {
return
}
if err != nil {
c.log.Printf("[ERR] mdns: Failed to read packet: %v", err)
continue
}
msg := new(dns.Msg)
if err := msg.Unpack(buf[:n]); err != nil {
c.log.Printf("[ERR] mdns: Failed to unpack packet: %v", err)
continue
}
select {
case msgCh <- &msgAddr{
msg: msg,
src: addr,
}:
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
}

View file

@ -0,0 +1,124 @@
package mdns
import (
"errors"
"fmt"
"io/ioutil"
"net"
"os"
"strings"
"github.com/miekg/dns"
yaml "gopkg.in/yaml.v3"
)
type multiService struct {
services []*MDNSService
}
func (m multiService) Records(q dns.Question) []dns.RR {
records := make([]dns.RR, 0)
for _, svc := range m.services {
records = append(records, svc.Records(q)...)
}
return records
}
type Advertiser struct {
Filename string
Mapping map[string]ServiceEntry
Service multiService
Server *Server
}
func (mod *MDNSModule) startAdvertiser(fileName string) error {
if mod.advertiser != nil {
return fmt.Errorf("advertiser already started for %s", mod.advertiser.Filename)
}
data, err := ioutil.ReadFile(fileName)
if err != nil {
return fmt.Errorf("could not read %s: %v", fileName, err)
}
mapping := make(map[string]ServiceEntry)
if err = yaml.Unmarshal(data, &mapping); err != nil {
return fmt.Errorf("could not deserialize %s: %v", fileName, err)
}
hostName, err := os.Hostname()
if err != nil {
return fmt.Errorf("could not get hostname: %v", err)
}
if !strings.HasSuffix(hostName, ".") {
hostName += "."
}
mod.Info("loaded %d services from %s, advertising with: host=%s ipv4=%s ipv6=%s",
len(mapping),
fileName,
hostName,
mod.Session.Interface.IpAddress,
mod.Session.Interface.Ip6Address)
advertiser := &Advertiser{
Filename: fileName,
Mapping: mapping,
Service: multiService{
services: make([]*MDNSService, 0),
},
}
for _, svcData := range mapping {
svcParts := strings.SplitN(svcData.Name, ".", 2)
svcInstance := svcParts[0]
svcService := strings.Replace(svcParts[1], ".local.", "", 1)
// TODO: patch UUID
service, err := NewMDNSService(
mod,
svcInstance,
svcService,
"local.",
hostName,
svcData.Port,
[]net.IP{
mod.Session.Interface.IP,
mod.Session.Interface.IPv6,
},
svcData.InfoFields)
if err != nil {
return fmt.Errorf("could not create service %s: %v", svcData.Name, err)
}
advertiser.Service.services = append(advertiser.Service.services, service)
}
if advertiser.Server, err = NewServer(mod, &Config{Zone: advertiser.Service}); err != nil {
return fmt.Errorf("could not create server: %v", err)
}
mod.advertiser = advertiser
mod.Debug("%+v", *mod.advertiser)
return nil
}
func (mod *MDNSModule) stopAdvertiser() error {
if mod.advertiser == nil {
return errors.New("advertiser not started")
}
mod.Info("stopping %d services ...", len(mod.advertiser.Mapping))
mod.advertiser.Server.Shutdown()
mod.advertiser = nil
return nil
}

View file

@ -0,0 +1,265 @@
package mdns
import (
"fmt"
"strings"
"time"
"github.com/bettercap/bettercap/v2/modules/syn_scan"
"github.com/bettercap/bettercap/v2/network"
"github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/str"
"github.com/evilsocket/islazy/tui"
)
type MDNSModule struct {
session.SessionModule
advertiser *Advertiser
discoChannel chan *ServiceEntry
mapping map[string]map[string]*ServiceEntry
}
func NewMDNSModule(s *session.Session) *MDNSModule {
mod := &MDNSModule{
SessionModule: session.NewSessionModule("mdns", s),
discoChannel: make(chan *ServiceEntry),
mapping: make(map[string]map[string]*ServiceEntry),
advertiser: nil,
}
mod.SessionModule.Requires("net.recon")
mod.AddHandler(session.NewModuleHandler("mdns.discovery on", "",
"Start DNS-SD / mDNS discovery.",
func(args []string) error {
return mod.Start()
}))
mod.AddHandler(session.NewModuleHandler("mdns.discovery off", "",
"Stop DNS-SD / mDNS discovery.",
func(args []string) error {
return mod.Stop()
}))
// TODO: add autocomplete
mod.AddHandler(session.NewModuleHandler("mdns.show", "",
"Show discovered services.",
func(args []string) error {
return mod.show("", false)
}))
mod.AddHandler(session.NewModuleHandler("mdns.show-full", "",
"Show discovered services and their DNS records.",
func(args []string) error {
return mod.show("", true)
}))
mod.AddHandler(session.NewModuleHandler("mdns.show ADDRESS", "mdns.show (.+)",
"Show discovered services given an ip address.",
func(args []string) error {
return mod.show(args[0], false)
}))
mod.AddHandler(session.NewModuleHandler("mdns.show-full ADDRESS", "mdns.show-full (.+)",
"Show discovered services and DNS records given an ip address.",
func(args []string) error {
return mod.show(args[0], true)
}))
mod.AddHandler(session.NewModuleHandler("mdns.save ADDRESS FILENAME", "mdns.save (.+) (.+)",
"Save the mDNS information of a given ADDRESS in the FILENAME yaml file.",
func(args []string) error {
return mod.save(args[0], args[1])
}))
mod.AddHandler(session.NewModuleHandler("mdns.advertise FILENAME", "mdns.advertise (.+)",
"Start advertising the mDNS services from the FILENAME yaml file.",
func(args []string) error {
if args[0] == "off" {
return mod.stopAdvertiser()
}
return mod.startAdvertiser(args[0])
}))
mod.AddHandler(session.NewModuleHandler("mdns.advertise off", "",
"Start a previously started advertiser.",
func(args []string) error {
return mod.stopAdvertiser()
}))
return mod
}
func (mod *MDNSModule) Name() string {
return "mdns"
}
func (mod *MDNSModule) Description() string {
return "A DNS-SD / mDNS module for discovery and spoofing."
}
func (mod *MDNSModule) Author() string {
return "Simone Margaritelli <evilsocket@gmail.com>"
}
func (mod *MDNSModule) Configure() (err error) {
if mod.Running() {
return session.ErrAlreadyStarted(mod.Name())
}
if mod.discoChannel != nil {
close(mod.discoChannel)
}
mod.discoChannel = make(chan *ServiceEntry)
mod.mapping = make(map[string]map[string]*ServiceEntry)
return
}
type ServiceDiscoveryEvent struct {
Service ServiceEntry `json:"service"`
Endpoint *network.Endpoint `json:"endpoint"`
}
func (mod *MDNSModule) updateEndpointMeta(address string, endpoint *network.Endpoint, svc *ServiceEntry) {
mod.Debug("found endpoint %s for address %s", endpoint.HwAddress, address)
// update mdns metadata
meta := make(map[string]string)
svcType := strings.SplitN(svc.Name, ".", 2)[1]
meta[fmt.Sprintf("mdns:%s:name", svcType)] = svc.Name
meta[fmt.Sprintf("mdns:%s:hostname", svcType)] = svc.Host
if svc.AddrV4 != nil {
meta[fmt.Sprintf("mdns:%s:ipv4", svcType)] = svc.AddrV4.String()
}
if svc.AddrV6 != nil {
meta[fmt.Sprintf("mdns:%s:ipv6", svcType)] = svc.AddrV6.String()
}
meta[fmt.Sprintf("mdns:%s:port", svcType)] = fmt.Sprintf("%d", svc.Port)
for _, field := range svc.InfoFields {
field = str.Trim(field)
if len(field) == 0 {
continue
}
key := ""
value := ""
if strings.Contains(field, "=") {
parts := strings.SplitN(field, "=", 2)
key = parts[0]
value = parts[1]
} else {
key = field
}
meta[fmt.Sprintf("mdns:%s:info:%s", svcType, key)] = value
}
mod.Debug("meta for %s: %v", address, meta)
endpoint.OnMeta(meta)
// update ports
ports := endpoint.Meta.GetOr("ports", map[int]*syn_scan.OpenPort{}).(map[int]*syn_scan.OpenPort)
if _, found := ports[svc.Port]; !found {
ports[svc.Port] = &syn_scan.OpenPort{
Proto: "tcp",
Port: svc.Port,
Service: network.GetServiceByPort(svc.Port, "tcp"),
}
}
endpoint.Meta.Set("ports", ports)
}
func (mod *MDNSModule) onServiceDiscovered(svc *ServiceEntry) {
mod.Debug("discovered service %s (%s) [%v / %v]:%d", tui.Green(svc.Name), tui.Dim(svc.Host), svc.AddrV4, svc.AddrV6, svc.Port)
event := ServiceDiscoveryEvent{
Service: *svc,
Endpoint: nil,
}
addresses := []string{}
if svc.AddrV4 != nil {
addresses = append(addresses, svc.AddrV4.String())
}
if svc.AddrV6 != nil {
addresses = append(addresses, svc.AddrV6.String())
}
for _, address := range addresses {
if event.Endpoint = mod.Session.Lan.GetByIp(address); event.Endpoint != nil {
// update endpoint metadata
mod.updateEndpointMeta(address, event.Endpoint, svc)
// update internal module mapping
if ipServices, found := mod.mapping[address]; found {
ipServices[svc.Name] = svc
} else {
mod.mapping[address] = map[string]*ServiceEntry{
svc.Name: svc,
}
}
break
} else {
mod.Warning("got mdns entry for unknown ip %s", svc.AddrV4)
}
}
session.I.Events.Add("mdns.service", event)
session.I.Refresh()
}
func (mod *MDNSModule) Start() (err error) {
if err = mod.Configure(); err != nil {
return err
}
// start the discovery
service := "_services._dns-sd._udp"
params := DefaultParams(service)
params.Module = mod
params.Service = service
params.Domain = "local"
params.Entries = mod.discoChannel
params.DisableIPv6 = true // https://github.com/hashicorp/mdns/issues/35
params.Timeout = time.Duration(10) * time.Minute
go func() {
mod.Info("starting query routine ...")
if err := Query(params); err != nil {
mod.Error("service discovery query: %v", err)
}
mod.Info("stopping query routine ...")
}()
return mod.SetRunning(true, func() {
mod.Info("mDNS service discovery started")
for entry := range mod.discoChannel {
mod.onServiceDiscovered(entry)
}
mod.Info("mDNS service discovery stopped")
})
}
func (mod *MDNSModule) Stop() error {
return mod.SetRunning(false, func() {
if mod.discoChannel != nil {
mod.Info("closing mDNS discovery channel")
close(mod.discoChannel)
mod.discoChannel = nil
}
})
}

35
modules/mdns/mdns_save.go Normal file
View file

@ -0,0 +1,35 @@
package mdns
import (
"fmt"
"io/ioutil"
yaml "gopkg.in/yaml.v3"
)
func (mod *MDNSModule) save(address, filename string) error {
if address == "" {
return fmt.Errorf("address cannot be empty")
}
if filename == "" {
return fmt.Errorf("filename cannot be empty")
}
if ipServices, found := mod.mapping[address]; found {
data, err := yaml.Marshal(ipServices)
if err != nil {
return err
}
err = ioutil.WriteFile(filename, data, 0644)
if err != nil {
return err
}
mod.Info("mDNS information saved to %s", filename)
} else {
return fmt.Errorf("no mDNS information found for address %s", address)
}
return nil
}

75
modules/mdns/mdns_show.go Normal file
View file

@ -0,0 +1,75 @@
package mdns
import (
"fmt"
"sort"
"github.com/evilsocket/islazy/str"
"github.com/evilsocket/islazy/tui"
)
type entry struct {
ip string
services map[string]*ServiceEntry
}
func (mod *MDNSModule) show(filter string, withData bool) error {
fmt.Fprintf(mod.Session.Events.Stdout, "\n")
// convert to list for sorting
entries := make([]entry, 0)
for ip, services := range mod.mapping {
if filter == "" || ip == filter {
entries = append(entries, entry{ip, services})
}
}
sort.Slice(entries, func(i, j int) bool {
return entries[i].ip < entries[j].ip
})
for _, entry := range entries {
if endpoint := mod.Session.Lan.GetByIp(entry.ip); endpoint != nil {
fmt.Fprintf(mod.Session.Events.Stdout, "* %s (%s)\n", endpoint.IpAddress, tui.Dim(endpoint.Vendor))
} else {
fmt.Fprintf(mod.Session.Events.Stdout, "* %s\n", tui.Bold(entry.ip))
}
for name, svc := range entry.services {
fmt.Fprintf(mod.Session.Events.Stdout, " %s (%s) [%v / %v]:%s\n",
tui.Green(name),
tui.Dim(svc.Host),
svc.AddrV4,
svc.AddrV6,
tui.Red(fmt.Sprintf("%d", svc.Port)),
)
numFields := len(svc.InfoFields)
if withData {
if numFields > 0 {
for _, field := range svc.InfoFields {
if field = str.Trim(field); len(field) > 0 {
fmt.Fprintf(mod.Session.Events.Stdout, " %s\n", field)
}
}
} else {
fmt.Fprintf(mod.Session.Events.Stdout, " %s\n", tui.Dim("no data"))
}
} else {
if numFields > 0 {
fmt.Fprintf(mod.Session.Events.Stdout, " <%d records>\n", numFields)
} else {
fmt.Fprintf(mod.Session.Events.Stdout, " %s\n", tui.Dim("<no records>"))
}
}
}
fmt.Fprintf(mod.Session.Events.Stdout, "\n")
}
if len(entries) > 0 {
mod.Session.Refresh()
}
return nil
}

306
modules/mdns/server.go Normal file
View file

@ -0,0 +1,306 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MIT
package mdns
import (
"fmt"
"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 {
mod *MDNSModule
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(mod *MDNSModule, config *Config) (*Server, error) {
// Create the listeners
ipv4List, err := net.ListenMulticastUDP("udp4", config.Iface, ipv4Addr)
if err != nil {
return nil, err
}
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{
mod: mod,
config: config,
ipv4List: ipv4List,
ipv6List: ipv6List,
shutdownCh: make(chan struct{}),
}
if ipv4List != nil {
mod.Info("starting ipv4 receiver for %v", s.ipv4List)
go s.recv(s.ipv4List)
}
if ipv6List != nil {
mod.Info("starting ipv6 receiver for %v", s.ipv6List)
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 {
s.mod.Debug("receiving from %v ...", c)
n, from, err := c.ReadFrom(buf)
if err != nil {
s.mod.Error("error while receiving datagram: %v", err)
continue
}
if err := s.parsePacket(buf[:n], from); err != nil {
s.mod.Debug("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 {
s.mod.Error("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
}
s.mod.Warning("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
}
s.mod.Info("%+v :", q)
for _, rec := range records {
s.mod.Info(" %+v", rec)
}
// 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 {
s.mod.Debug("sending response=%v from=%v", *resp, from)
// 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
}
}

317
modules/mdns/service.go Normal file
View file

@ -0,0 +1,317 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MIT
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 {
mod *MDNSModule
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(mod *MDNSModule, 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)
}
}
mod.Debug("serviceAddr=%s.%s.", trimDot(service), trimDot(domain))
mod.Debug("instanceAddr=%s.%s.%s.", instance, trimDot(service), trimDot(domain))
mod.Debug("enumAddr=_services._dns-sd._udp.%s.", trimDot(domain))
return &MDNSService{
mod: mod,
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
}

View file

@ -1,160 +0,0 @@
package mdns_server
import (
"fmt"
"io/ioutil"
"log"
"net"
"os"
"github.com/bettercap/bettercap/v2/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()
})
}

View file

@ -19,7 +19,7 @@ import (
"github.com/bettercap/bettercap/v2/modules/https_proxy"
"github.com/bettercap/bettercap/v2/modules/https_server"
"github.com/bettercap/bettercap/v2/modules/mac_changer"
"github.com/bettercap/bettercap/v2/modules/mdns_server"
"github.com/bettercap/bettercap/v2/modules/mdns"
"github.com/bettercap/bettercap/v2/modules/mysql_server"
"github.com/bettercap/bettercap/v2/modules/ndp_spoof"
"github.com/bettercap/bettercap/v2/modules/net_probe"
@ -55,7 +55,7 @@ func LoadModules(sess *session.Session) {
sess.Register(https_server.NewHttpsServer(sess))
sess.Register(mac_changer.NewMacChanger(sess))
sess.Register(mysql_server.NewMySQLServer(sess))
sess.Register(mdns_server.NewMDNSServer(sess))
sess.Register(mdns.NewMDNSModule(sess))
sess.Register(net_sniff.NewSniffer(sess))
sess.Register(packet_proxy.NewPacketProxy(sess))
sess.Register(net_probe.NewProber(sess))

View file

@ -118,7 +118,7 @@ func (mod *Prober) Start() error {
}
if mod.probes.MDNS {
go mod.mdnsProber()
mod.Session.Run("mdns.discovery on")
}
fromIP := mod.Session.Interface.IP
@ -129,9 +129,6 @@ func (mod *Prober) Start() error {
mod.Info("probing %d addresses on %s", len(addresses), cidr)
for mod.Running() {
if mod.probes.MDNS {
mod.sendProbeMDNS(fromIP, fromHW)
}
if mod.probes.UPNP {
mod.sendProbeUPNP(fromIP, fromHW)
@ -160,6 +157,10 @@ func (mod *Prober) Start() error {
func (mod *Prober) Stop() error {
return mod.SetRunning(false, func() {
if mod.probes.MDNS {
mod.Session.Run("mdns.discovery off")
}
mod.waitGroup.Wait()
})
}

View file

@ -1,100 +0,0 @@
package net_probe
import (
"fmt"
"io/ioutil"
"log"
"net"
"github.com/bettercap/bettercap/v2/packets"
"github.com/hashicorp/mdns"
)
var services = []string{
"_hap._tcp.local",
"_homekit._tcp.local",
"_airplay._tcp.local",
"_raop._tcp.local",
"_sleep-proxy._udp.local",
"_companion-link._tcp.local",
"_googlezone._tcp.local",
"_googlerpc._tcp.local",
"_googlecast._tcp.local",
"local",
}
func (mod *Prober) sendProbeMDNS(from net.IP, from_hw net.HardwareAddr) {
err, raw := packets.NewMDNSProbe(from, from_hw)
if err != nil {
mod.Error("error while sending mdns probe: %v", err)
return
} else if err := mod.Session.Queue.Send(raw); err != nil {
mod.Error("error sending mdns packet: %s", err)
} else {
mod.Debug("sent %d bytes of MDNS probe", len(raw))
}
}
func (mod *Prober) mdnsListener(c chan *mdns.ServiceEntry) {
mod.Debug("mdns listener started")
defer mod.Debug("mdns listener stopped")
for entry := range c {
addrs := []string{}
if entry.AddrV4 != nil {
addrs = append(addrs, entry.AddrV4.String())
}
if entry.AddrV6 != nil {
addrs = append(addrs, entry.AddrV6.String())
}
for _, addr := range addrs {
if host := mod.Session.Lan.GetByIp(addr); host != nil {
meta := make(map[string]string)
meta["mdns:name"] = entry.Name
meta["mdns:hostname"] = entry.Host
if entry.AddrV4 != nil {
meta["mdns:ipv4"] = entry.AddrV4.String()
}
if entry.AddrV6 != nil {
meta["mdns:ipv6"] = entry.AddrV6.String()
}
meta["mdns:port"] = fmt.Sprintf("%d", entry.Port)
mod.Debug("meta for %s: %v", addr, meta)
host.OnMeta(meta)
} else {
mod.Debug("got mdns entry for unknown ip %s", entry.AddrV4)
}
}
}
}
func (mod *Prober) mdnsProber() {
mod.Debug("mdns prober started")
defer mod.Debug("mdns.prober stopped")
mod.waitGroup.Add(1)
defer mod.waitGroup.Done()
log.SetOutput(ioutil.Discard)
ch := make(chan *mdns.ServiceEntry)
defer close(ch)
go mod.mdnsListener(ch)
for mod.Running() {
for _, svc := range services {
if mod.Running() {
mdns.Lookup(svc, ch)
}
}
}
}

View file

@ -84,7 +84,7 @@ func NewMDNSProbe(from net.IP, from_hw net.HardwareAddr) (error, []byte) {
OpCode: layers.DNSOpCodeQuery,
Questions: []layers.DNSQuestion{
{
Name: []byte("_services._dns-sd._udp.local"),
Name: []byte("_services._dns-sd._udp.local."),
Type: layers.DNSTypePTR,
Class: layers.DNSClassIN,
},

199
printer.yml Normal file
View file

@ -0,0 +1,199 @@
EPSON\ XP-666\ Series._http._tcp.local.:
name: EPSON\ XP-666\ Series._http._tcp.local.
host: EPSON59F5BA.local.
addrv4: 192.168.50.21
addrv6: fe80::46d2:44ff:fe59:f5ba
addrv6ipaddr:
ip: fe80::46d2:44ff:fe59:f5ba
zone: ""
port: 80
info: ""
infofields:
- ""
addr: fe80::46d2:44ff:fe59:f5ba
EPSON\ XP-666\ Series._ipp._tcp.local.:
name: EPSON\ XP-666\ Series._ipp._tcp.local.
host: EPSON59F5BA.local.
addrv4: 192.168.50.21
addrv6: fe80::46d2:44ff:fe59:f5ba
addrv6ipaddr:
ip: fe80::46d2:44ff:fe59:f5ba
zone: ""
port: 631
info: txtvers=1|ty=EPSON XP-666 Series|usb_MFG=EPSON|usb_MDL=XP-666 Series|product=(EPSON XP-666 Series)|pdl=application/octet-stream,image/pwg-raster,image/urf,image/jpeg|rp=ipp/print|qtotal=1|Color=T|Duplex=T|Scan=T|Fax=F|kind=document,envelope,label,photo|PaperMax=legal-A4|URF=CP1,MT1-3-5-8-10-11-12,PQ4-5,OB9,OFU0,RS360,SRGB24,W8,DM3,IS1-7,V1.4|mopria-certified=1.2|priority=30|adminurl=http://EPSON59F5BA.local.:80/PRESENTATION/BONJOUR|note=|UUID=cff92100-67f4-11d4-a45f-44d24459f5ba|TLS=1.2
infofields:
- txtvers=1
- ty=EPSON XP-666 Series
- usb_MFG=EPSON
- usb_MDL=XP-666 Series
- product=(EPSON XP-666 Series)
- pdl=application/octet-stream,image/pwg-raster,image/urf,image/jpeg
- rp=ipp/print
- qtotal=1
- Color=T
- Duplex=T
- Scan=T
- Fax=F
- kind=document,envelope,label,photo
- PaperMax=legal-A4
- URF=CP1,MT1-3-5-8-10-11-12,PQ4-5,OB9,OFU0,RS360,SRGB24,W8,DM3,IS1-7,V1.4
- mopria-certified=1.2
- priority=30
- adminurl=http://EPSON59F5BA.local.:80/PRESENTATION/BONJOUR
- note=
- UUID=cff92100-67f4-11d4-a45f-44d24459f5ba
- TLS=1.2
addr: fe80::46d2:44ff:fe59:f5ba
EPSON\ XP-666\ Series._ipps._tcp.local.:
name: EPSON\ XP-666\ Series._ipps._tcp.local.
host: EPSON59F5BA.local.
addrv4: 192.168.50.21
addrv6: fe80::46d2:44ff:fe59:f5ba
addrv6ipaddr:
ip: fe80::46d2:44ff:fe59:f5ba
zone: ""
port: 631
info: txtvers=1|ty=EPSON XP-666 Series|usb_MFG=EPSON|usb_MDL=XP-666 Series|product=(EPSON XP-666 Series)|pdl=application/octet-stream,image/pwg-raster,image/urf,image/jpeg|rp=ipp/print|qtotal=1|Color=T|Duplex=T|Scan=T|Fax=F|kind=document,envelope,label,photo|PaperMax=legal-A4|URF=CP1,MT1-3-5-8-10-11-12,PQ4-5,OB9,OFU0,RS360,SRGB24,W8,DM3,IS1-7,V1.4|mopria-certified=1.2|priority=30|adminurl=http://EPSON59F5BA.local.:80/PRESENTATION/BONJOUR|note=|UUID=cff92100-67f4-11d4-a45f-44d24459f5ba|TLS=1.2
infofields:
- txtvers=1
- ty=EPSON XP-666 Series
- usb_MFG=EPSON
- usb_MDL=XP-666 Series
- product=(EPSON XP-666 Series)
- pdl=application/octet-stream,image/pwg-raster,image/urf,image/jpeg
- rp=ipp/print
- qtotal=1
- Color=T
- Duplex=T
- Scan=T
- Fax=F
- kind=document,envelope,label,photo
- PaperMax=legal-A4
- URF=CP1,MT1-3-5-8-10-11-12,PQ4-5,OB9,OFU0,RS360,SRGB24,W8,DM3,IS1-7,V1.4
- mopria-certified=1.2
- priority=30
- adminurl=http://EPSON59F5BA.local.:80/PRESENTATION/BONJOUR
- note=
- UUID=cff92100-67f4-11d4-a45f-44d24459f5ba
- TLS=1.2
addr: fe80::46d2:44ff:fe59:f5ba
EPSON\ XP-666\ Series._pdl-datastream._tcp.local.:
name: EPSON\ XP-666\ Series._pdl-datastream._tcp.local.
host: EPSON59F5BA.local.
addrv4: 192.168.50.21
addrv6: fe80::46d2:44ff:fe59:f5ba
addrv6ipaddr:
ip: fe80::46d2:44ff:fe59:f5ba
zone: ""
port: 9100
info: txtvers=1|priority=40|ty=EPSON XP-666 Series|usb_MFG=EPSON|usb_MDL=XP-666 Series|product=(EPSON XP-666 Series)|pdl=raw|qtotal=1|adminurl=http://EPSON59F5BA.local.:80/PRESENTATION/BONJOUR|note=
infofields:
- txtvers=1
- priority=40
- ty=EPSON XP-666 Series
- usb_MFG=EPSON
- usb_MDL=XP-666 Series
- product=(EPSON XP-666 Series)
- pdl=raw
- qtotal=1
- adminurl=http://EPSON59F5BA.local.:80/PRESENTATION/BONJOUR
- note=
addr: fe80::46d2:44ff:fe59:f5ba
EPSON\ XP-666\ Series._printer._tcp.local.:
name: EPSON\ XP-666\ Series._printer._tcp.local.
host: EPSON59F5BA.local.
addrv4: 192.168.50.21
addrv6: fe80::46d2:44ff:fe59:f5ba
addrv6ipaddr:
ip: fe80::46d2:44ff:fe59:f5ba
zone: ""
port: 515
info: txtvers=1|priority=50|ty=EPSON XP-666 Series|usb_MFG=EPSON|usb_MDL=XP-666 Series|product=(EPSON XP-666 Series)|pdl=raw|rp=auto|qtotal=1|adminurl=http://EPSON59F5BA.local.:80/PRESENTATION/BONJOUR|note=
infofields:
- txtvers=1
- priority=50
- ty=EPSON XP-666 Series
- usb_MFG=EPSON
- usb_MDL=XP-666 Series
- product=(EPSON XP-666 Series)
- pdl=raw
- rp=auto
- qtotal=1
- adminurl=http://EPSON59F5BA.local.:80/PRESENTATION/BONJOUR
- note=
addr: fe80::46d2:44ff:fe59:f5ba
EPSON\ XP-666\ Series._privet._tcp.local.:
name: EPSON\ XP-666\ Series._privet._tcp.local.
host: EPSON59F5BA.local.
addrv4: 192.168.50.21
addrv6: fe80::46d2:44ff:fe59:f5ba
addrv6ipaddr:
ip: fe80::46d2:44ff:fe59:f5ba
zone: ""
port: 80
info: txtvers=1|ty=EPSON XP-666 Series (EPSON59F5BA)|url=https://www.google.com/cloudprint|type=printer|id=0936a89f-33d7-80f5-c1bc-7421d40a78b5|cs=offline
infofields:
- txtvers=1
- ty=EPSON XP-666 Series (EPSON59F5BA)
- url=https://www.google.com/cloudprint
- type=printer
- id=0936a89f-33d7-80f5-c1bc-7421d40a78b5
- cs=offline
addr: fe80::46d2:44ff:fe59:f5ba
EPSON\ XP-666\ Series._scanner._tcp.local.:
name: EPSON\ XP-666\ Series._scanner._tcp.local.
host: EPSON59F5BA.local.
addrv4: 192.168.50.21
addrv6: fe80::46d2:44ff:fe59:f5ba
addrv6ipaddr:
ip: fe80::46d2:44ff:fe59:f5ba
zone: ""
port: 1865
info: txtvers=1|ty=EPSON XP-666 Series|adminurl=http://EPSON59F5BA.local.:80/PRESENTATION/BONJOUR|mfg=EPSON|mdl=XP-666 Series|UUID=cff92100-67f4-11d4-a45f-44d24459f5ba|scannerAvailable=0|note=
infofields:
- txtvers=1
- ty=EPSON XP-666 Series
- adminurl=http://EPSON59F5BA.local.:80/PRESENTATION/BONJOUR
- mfg=EPSON
- mdl=XP-666 Series
- UUID=cff92100-67f4-11d4-a45f-44d24459f5ba
- scannerAvailable=0
- note=
addr: fe80::46d2:44ff:fe59:f5ba
EPSON\ XP-666\ Series._smb._tcp.local.:
name: EPSON\ XP-666\ Series._smb._tcp.local.
host: EPSON59F5BA.local.
addrv4: 192.168.50.21
addrv6: fe80::46d2:44ff:fe59:f5ba
addrv6ipaddr:
ip: fe80::46d2:44ff:fe59:f5ba
zone: ""
port: 445
info: ""
infofields:
- ""
addr: fe80::46d2:44ff:fe59:f5ba
EPSON\ XP-666\ Series._uscan._tcp.local.:
name: EPSON\ XP-666\ Series._uscan._tcp.local.
host: EPSON59F5BA.local.
addrv4: 192.168.50.21
addrv6: fe80::46d2:44ff:fe59:f5ba
addrv6ipaddr:
ip: fe80::46d2:44ff:fe59:f5ba
zone: ""
port: 443
info: txtvers=1|vers=2.5|representation=/PRESENTATION/AIRPRINT/PRINTER_128.PNG|rs=eSCL|ty=EPSON XP-666 Series|pdl=application/pdf,image/jpeg|cs=color,grayscale,binary|is=platen|duplex=F|adminurl=http://EPSON59F5BA.local.:80/PRESENTATION/BONJOUR|UUID=cff92100-67f4-11d4-a45f-44d24459f5ba|note=
infofields:
- txtvers=1
- vers=2.5
- representation=/PRESENTATION/AIRPRINT/PRINTER_128.PNG
- rs=eSCL
- ty=EPSON XP-666 Series
- pdl=application/pdf,image/jpeg
- cs=color,grayscale,binary
- is=platen
- duplex=F
- adminurl=http://EPSON59F5BA.local.:80/PRESENTATION/BONJOUR
- UUID=cff92100-67f4-11d4-a45f-44d24459f5ba
- note=
addr: fe80::46d2:44ff:fe59:f5ba