From 67cc9680edfdeaf11e19ad468cffbe32919736b4 Mon Sep 17 00:00:00 2001 From: Simone Margaritelli Date: Wed, 18 Sep 2024 23:21:30 +0200 Subject: [PATCH] progress --- go.mod | 2 +- go.sum | 9 +- modules/events_stream/events_view.go | 2 + modules/events_stream/events_view_mdns.go | 23 ++ modules/mdns/client.go | 475 ++++++++++++++++++++++ modules/mdns/mdns_advertise.go | 124 ++++++ modules/mdns/mdns_discovery.go | 265 ++++++++++++ modules/mdns/mdns_save.go | 35 ++ modules/mdns/mdns_show.go | 75 ++++ modules/mdns/server.go | 306 ++++++++++++++ modules/mdns/service.go | 317 +++++++++++++++ modules/mdns_server/mdns_server.go | 160 -------- modules/modules.go | 4 +- modules/net_probe/net_probe.go | 9 +- modules/net_probe/net_probe_mdns.go | 100 ----- packets/mdns.go | 2 +- printer.yml | 199 +++++++++ 17 files changed, 1833 insertions(+), 274 deletions(-) create mode 100644 modules/events_stream/events_view_mdns.go create mode 100644 modules/mdns/client.go create mode 100644 modules/mdns/mdns_advertise.go create mode 100644 modules/mdns/mdns_discovery.go create mode 100644 modules/mdns/mdns_save.go create mode 100644 modules/mdns/mdns_show.go create mode 100644 modules/mdns/server.go create mode 100644 modules/mdns/service.go delete mode 100644 modules/mdns_server/mdns_server.go delete mode 100644 modules/net_probe/net_probe_mdns.go create mode 100644 printer.yml diff --git a/go.mod b/go.mod index bee884cb..8620fff3 100644 --- a/go.mod +++ b/go.mod @@ -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 ( diff --git a/go.sum b/go.sum index 0f76f284..fd09f526 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/modules/events_stream/events_view.go b/modules/events_stream/events_view.go index b8bec894..c6e1b397 100644 --- a/modules/events_stream/events_view.go +++ b/modules/events_stream/events_view.go @@ -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) } diff --git a/modules/events_stream/events_view_mdns.go b/modules/events_stream/events_view_mdns.go new file mode 100644 index 00000000..dffbb158 --- /dev/null +++ b/modules/events_stream/events_view_mdns.go @@ -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, + ) +} diff --git a/modules/mdns/client.go b/modules/mdns/client.go new file mode 100644 index 00000000..02f4200a --- /dev/null +++ b/modules/mdns/client.go @@ -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 +} diff --git a/modules/mdns/mdns_advertise.go b/modules/mdns/mdns_advertise.go new file mode 100644 index 00000000..ce4884de --- /dev/null +++ b/modules/mdns/mdns_advertise.go @@ -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 +} diff --git a/modules/mdns/mdns_discovery.go b/modules/mdns/mdns_discovery.go new file mode 100644 index 00000000..6fbfabb6 --- /dev/null +++ b/modules/mdns/mdns_discovery.go @@ -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 " +} + +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 + } + }) +} diff --git a/modules/mdns/mdns_save.go b/modules/mdns/mdns_save.go new file mode 100644 index 00000000..d1d099b2 --- /dev/null +++ b/modules/mdns/mdns_save.go @@ -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 +} diff --git a/modules/mdns/mdns_show.go b/modules/mdns/mdns_show.go new file mode 100644 index 00000000..fa5c7a15 --- /dev/null +++ b/modules/mdns/mdns_show.go @@ -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("")) + } + } + } + + fmt.Fprintf(mod.Session.Events.Stdout, "\n") + } + + if len(entries) > 0 { + mod.Session.Refresh() + } + + return nil +} diff --git a/modules/mdns/server.go b/modules/mdns/server.go new file mode 100644 index 00000000..66a49184 --- /dev/null +++ b/modules/mdns/server.go @@ -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 + } +} diff --git a/modules/mdns/service.go b/modules/mdns/service.go new file mode 100644 index 00000000..24078762 --- /dev/null +++ b/modules/mdns/service.go @@ -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. +} + +// 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 +} diff --git a/modules/mdns_server/mdns_server.go b/modules/mdns_server/mdns_server.go deleted file mode 100644 index 163a003f..00000000 --- a/modules/mdns_server/mdns_server.go +++ /dev/null @@ -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 " -} - -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() - }) -} diff --git a/modules/modules.go b/modules/modules.go index 02fae46d..60eb6264 100644 --- a/modules/modules.go +++ b/modules/modules.go @@ -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)) diff --git a/modules/net_probe/net_probe.go b/modules/net_probe/net_probe.go index 15f0783e..d62da6b4 100644 --- a/modules/net_probe/net_probe.go +++ b/modules/net_probe/net_probe.go @@ -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() }) } diff --git a/modules/net_probe/net_probe_mdns.go b/modules/net_probe/net_probe_mdns.go deleted file mode 100644 index a4c78614..00000000 --- a/modules/net_probe/net_probe_mdns.go +++ /dev/null @@ -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) - } - } - } -} diff --git a/packets/mdns.go b/packets/mdns.go index 2de3a383..2bcddf5a 100644 --- a/packets/mdns.go +++ b/packets/mdns.go @@ -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, }, diff --git a/printer.yml b/printer.yml new file mode 100644 index 00000000..c891bcb1 --- /dev/null +++ b/printer.yml @@ -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 \ No newline at end of file