From a49d561dcea3a3d1ed16fcd61c3288207fd6e4ea Mon Sep 17 00:00:00 2001 From: buffermet <29265684+buffermet@users.noreply.github.com> Date: Fri, 4 Oct 2024 03:00:54 +0200 Subject: [PATCH 01/10] init dns.proxy --- modules/dns_proxy/dns_proxy.go | 124 +++++++++++ modules/dns_proxy/dns_proxy_base.go | 194 ++++++++++++++++ modules/dns_proxy/dns_proxy_base_filters.go | 86 ++++++++ modules/dns_proxy/dns_proxy_js_query.go | 231 ++++++++++++++++++++ modules/dns_proxy/dns_proxy_script.go | 99 +++++++++ modules/modules.go | 2 + 6 files changed, 736 insertions(+) create mode 100644 modules/dns_proxy/dns_proxy.go create mode 100644 modules/dns_proxy/dns_proxy_base.go create mode 100644 modules/dns_proxy/dns_proxy_base_filters.go create mode 100644 modules/dns_proxy/dns_proxy_js_query.go create mode 100644 modules/dns_proxy/dns_proxy_script.go diff --git a/modules/dns_proxy/dns_proxy.go b/modules/dns_proxy/dns_proxy.go new file mode 100644 index 00000000..e737504b --- /dev/null +++ b/modules/dns_proxy/dns_proxy.go @@ -0,0 +1,124 @@ +package dns_proxy + +import ( + "github.com/bettercap/bettercap/v2/session" +) + +type DnsProxy struct { + session.SessionModule + proxy *DNSProxy +} + +func (mod *DnsProxy) Author() string { + return "Yarwin Kolff <@buffermet>" +} + +func (mod *DnsProxy) Configure() error { + var err error + var address string + var dnsPort int + var doRedirect bool + var nameserver string + var netProtocol string + var proxyPort int + var scriptPath string + + if mod.Running() { + return session.ErrAlreadyStarted(mod.Name()) + } else if err, dnsPort = mod.IntParam("dns.port"); err != nil { + return err + } else if err, address = mod.StringParam("dns.proxy.address"); err != nil { + return err + } else if err, nameserver = mod.StringParam("dns.proxy.nameserver"); err != nil { + return err + } else if err, netProtocol = mod.StringParam("dns.proxy.networkprotocol"); err != nil { + return err + } else if err, proxyPort = mod.IntParam("dns.proxy.port"); err != nil { + return err + } else if err, doRedirect = mod.BoolParam("dns.proxy.redirect"); err != nil { + return err + } else if err, scriptPath = mod.StringParam("dns.proxy.script"); err != nil { + return err + } + + error := mod.proxy.Configure(address, dnsPort, doRedirect, nameserver, netProtocol, proxyPort, scriptPath) + + return error +} + +func (mod *DnsProxy) Description() string { + return "A full featured DNS proxy that can be used to manipulate DNS traffic." +} + +func (mod *DnsProxy) Name() string { + return "dns.proxy" +} + +func NewDnsProxy(s *session.Session) *DnsProxy { + mod := &DnsProxy{ + SessionModule: session.NewSessionModule("dns.proxy", s), + proxy: NewDNSProxy(s, "dns.proxy"), + } + + mod.AddParam(session.NewIntParameter("dns.port", + "53", + "DNS port to redirect when the proxy is activated.")) + + mod.AddParam(session.NewStringParameter("dns.proxy.address", + session.ParamIfaceAddress, + session.IPv4Validator, + "Address to bind the DNS proxy to.")) + + mod.AddParam(session.NewStringParameter("dns.proxy.nameserver", + "1.1.1.1", + session.IPv4Validator, + "DNS resolver address.")) + + mod.AddParam(session.NewStringParameter("dns.proxy.networkprotocol", + "udp", + "^(udp|tcp|tcp-tls)$", + "Network protocol for the DNS proxy server to use. Accepted values: udp, tcp, tcp-tls")) + + mod.AddParam(session.NewIntParameter("dns.proxy.port", + "8053", + "Port to bind the DNS proxy to.")) + + mod.AddParam(session.NewBoolParameter("dns.proxy.redirect", + "true", + "Enable or disable port redirection with iptables.")) + + mod.AddParam(session.NewStringParameter("dns.proxy.script", + "", + "", + "Path of a JS proxy script.")) + + mod.AddHandler(session.NewModuleHandler("dns.proxy on", "", + "Start the DNS proxy.", + func(args []string) error { + return mod.Start() + })) + + mod.AddHandler(session.NewModuleHandler("dns.proxy off", "", + "Stop the DNS proxy.", + func(args []string) error { + return mod.Stop() + })) + + return mod +} + +func (mod *DnsProxy) Start() error { + if err := mod.Configure(); err != nil { + return err + } + + return mod.SetRunning(true, func() { + mod.proxy.Start() + }) +} + +func (mod *DnsProxy) Stop() error { + return mod.SetRunning(false, func() { + mod.proxy.Stop() + }) +} diff --git a/modules/dns_proxy/dns_proxy_base.go b/modules/dns_proxy/dns_proxy_base.go new file mode 100644 index 00000000..1c619e95 --- /dev/null +++ b/modules/dns_proxy/dns_proxy_base.go @@ -0,0 +1,194 @@ +package dns_proxy + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/bettercap/bettercap/v2/firewall" + "github.com/bettercap/bettercap/v2/session" + "github.com/evilsocket/islazy/log" + + "github.com/miekg/dns" +) + +const ( + dialTimeout = 2 * time.Second + readTimeout = 2 * time.Second + writeTimeout = 2 * time.Second +) + +type DNSProxy struct { + Address string + Name string + Nameserver string + NetProtocol string + Redirection *firewall.Redirection + Script *DnsProxyScript + Server *dns.Server + Sess *session.Session + + doRedirect bool + isRunning bool + tag string +} + +func (p *DNSProxy) Configure(address string, dnsPort int, doRedirect bool, nameserver string, netProtocol string, proxyPort int, scriptPath string) error { + var err error + + p.Address = address + p.doRedirect = doRedirect + + if scriptPath != "" { + if err, p.Script = LoadDnsProxyScript(scriptPath, p.Sess); err != nil { + return err + } else { + p.Debug("proxy script %s loaded.", scriptPath) + } + } else { + p.Script = nil + } + + dnsClient := dns.Client{ + DialTimeout: dialTimeout, + Net: netProtocol, + ReadTimeout: readTimeout, + WriteTimeout: writeTimeout, + } + + resolverAddr := fmt.Sprintf("%s:%d", nameserver, dnsPort) + + handler := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) { + m := new(dns.Msg) + m.SetReply(req) + + clientIP := strings.Split(w.RemoteAddr().String(), ":")[0] + + req, res := p.onRequestFilter(req, clientIP) + if res == nil { + // unused var is time til res + res, _, err := dnsClient.Exchange(req, resolverAddr) + if err != nil { + p.Debug("error while resolving DNS query: %s", err.Error()) + m.SetRcode(req, dns.RcodeServerFailure) + w.WriteMsg(m) + return + } + res = p.onResponseFilter(req, res, clientIP) + if res == nil { + p.Error("response filter returned nil") + m.SetRcode(req, dns.RcodeServerFailure) + w.WriteMsg(m) + } else { + if err := w.WriteMsg(res); err != nil { + p.Error("Error writing response: %s", err) + } + } + } else { + if err := w.WriteMsg(res); err != nil { + p.Error("Error writing response: %s", err) + } + } + }) + + p.Server = &dns.Server{ + Addr: fmt.Sprintf("%s:%d", address, proxyPort), + Net: netProtocol, + Handler: handler, + } + + if p.doRedirect { + if !p.Sess.Firewall.IsForwardingEnabled() { + p.Info("enabling forwarding.") + p.Sess.Firewall.EnableForwarding(true) + } + + p.Redirection = firewall.NewRedirection(p.Sess.Interface.Name(), + netProtocol, + dnsPort, + p.Address, + proxyPort) + + if err := p.Sess.Firewall.EnableRedirection(p.Redirection, true); err != nil { + return err + } + + p.Debug("applied redirection %s", p.Redirection.String()) + } else { + p.Warning("port redirection disabled, the proxy must be set manually to work") + } + + p.Sess.UnkCmdCallback = func(cmd string) bool { + if p.Script != nil { + return p.Script.OnCommand(cmd) + } + return false + } + return nil +} + +func (p *DNSProxy) dnsWorker() error { + p.isRunning = true + return p.Server.ListenAndServe() +} + +func (p *DNSProxy) Debug(format string, args ...interface{}) { + p.Sess.Events.Log(log.DEBUG, p.tag+format, args...) +} + +func (p *DNSProxy) Info(format string, args ...interface{}) { + p.Sess.Events.Log(log.INFO, p.tag+format, args...) +} + +func (p *DNSProxy) Warning(format string, args ...interface{}) { + p.Sess.Events.Log(log.WARNING, p.tag+format, args...) +} + +func (p *DNSProxy) Error(format string, args ...interface{}) { + p.Sess.Events.Log(log.ERROR, p.tag+format, args...) +} + +func (p *DNSProxy) Fatal(format string, args ...interface{}) { + p.Sess.Events.Log(log.FATAL, p.tag+format, args...) +} + +func NewDNSProxy(s *session.Session, tag string) *DNSProxy { + p := &DNSProxy{ + Name: "dns.proxy", + Sess: s, + Server: nil, + doRedirect: true, + tag: session.AsTag(tag), + } + + return p +} + +func (p *DNSProxy) Start() { + go func() { + p.Info("started on %s", p.Server.Addr) + + err := p.dnsWorker() + // TODO: check the dns server closed error + if err != nil && err.Error() != "dns: Server closed" { + p.Fatal("%s", err) + } + }() +} + +func (p *DNSProxy) Stop() error { + if p.doRedirect && p.Redirection != nil { + p.Debug("disabling redirection %s", p.Redirection.String()) + if err := p.Sess.Firewall.EnableRedirection(p.Redirection, false); err != nil { + return err + } + p.Redirection = nil + } + + p.Sess.UnkCmdCallback = nil + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + return p.Server.ShutdownContext(ctx) +} diff --git a/modules/dns_proxy/dns_proxy_base_filters.go b/modules/dns_proxy/dns_proxy_base_filters.go new file mode 100644 index 00000000..5255b608 --- /dev/null +++ b/modules/dns_proxy/dns_proxy_base_filters.go @@ -0,0 +1,86 @@ +package dns_proxy + +import ( + "strings" + + "github.com/miekg/dns" +) + +func shortenResourceRecords(records []string) []string { + shorterRecords := []string{} + for _, record := range records { + shorterRecord := strings.ReplaceAll(record, "\t", " ") + shorterRecords = append(shorterRecords, shorterRecord) + } + return shorterRecords +} + +func (p *DNSProxy) logRequestAction(j *JSQuery) { + p.Sess.Events.Add(p.Name+".spoofed-request", struct { + Client string + Questions []string + }{ + j.Client["IP"], + shortenResourceRecords(j.Questions), + }) +} + +func (p *DNSProxy) logResponseAction(j *JSQuery) { + p.Sess.Events.Add(p.Name+".spoofed-response", struct { + client string + Extras []string + Answers []string + Nameservers []string + Questions []string + }{ + j.Client["IP"], + shortenResourceRecords(j.Extras), + shortenResourceRecords(j.Answers), + shortenResourceRecords(j.Nameservers), + shortenResourceRecords(j.Questions), + }) +} + +func (p *DNSProxy) onRequestFilter(query *dns.Msg, clientIP string) (req, res *dns.Msg) { + p.Debug("< %s %s", clientIP, query.Question) + + // do we have a proxy script? + if p.Script == nil { + return query, nil + } + + // run the module OnRequest callback if defined + jsreq, jsres := p.Script.OnRequest(query, clientIP) + if jsreq != nil { + // the request has been changed by the script + p.logRequestAction(jsreq) + return jsreq.ToQuery(), nil + } else if jsres != nil { + // a fake response has been returned by the script + p.logResponseAction(jsres) + return query, jsres.ToQuery() + } + + return query, nil +} + +func (p *DNSProxy) onResponseFilter(req, res *dns.Msg, clientIP string) *dns.Msg { + // sometimes it happens ¯\_(ツ)_/¯ + if res == nil { + return nil + } + + p.Debug("> %s %s", clientIP, res.Answer) + + // do we have a proxy script? + if p.Script != nil { + _, jsres := p.Script.OnResponse(req, res, clientIP) + if jsres != nil { + // the response has been changed by the script + p.logResponseAction(jsres) + return jsres.ToQuery() + } + } + + return res +} diff --git a/modules/dns_proxy/dns_proxy_js_query.go b/modules/dns_proxy/dns_proxy_js_query.go new file mode 100644 index 00000000..a13a6ef6 --- /dev/null +++ b/modules/dns_proxy/dns_proxy_js_query.go @@ -0,0 +1,231 @@ +package dns_proxy + +import ( + "fmt" + "regexp" + "strings" + + "github.com/bettercap/bettercap/v2/log" + "github.com/bettercap/bettercap/v2/session" + + "github.com/miekg/dns" +) + +var whiteSpaceRegexp = regexp.MustCompile(`\s+`) +var stripWhiteSpaceRegexp = regexp.MustCompile(`^\s*(.*?)\s*$`) + +type JSQuery struct { + Answers []string + Client map[string]string + Compress bool `json:"-"` + Extras []string + Header *JSQueryHeader + Nameservers []string + Questions []string + + refHash string +} + +type JSQueryHeader struct { + AuthenticatedData bool + Authoritative bool + CheckingDisabled bool + Id uint16 + Opcode int + Rcode int + RecursionAvailable bool + RecursionDesired bool + Response bool + Truncated bool + Zero bool +} + +func (j *JSQuery) NewHash() string { + headerHash := fmt.Sprintf("%t.%t.%t.%d.%d.%d.%t.%t.%t.%t.%t", + j.Header.AuthenticatedData, + j.Header.Authoritative, + j.Header.CheckingDisabled, + j.Header.Id, + j.Header.Opcode, + j.Header.Rcode, + j.Header.RecursionAvailable, + j.Header.RecursionDesired, + j.Header.Response, + j.Header.Truncated, + j.Header.Zero) + hash := fmt.Sprintf("%s.%s.%t.%s.%s.%s.%s", + strings.Join(j.Answers, ""), + j.Client["IP"], + j.Compress, + strings.Join(j.Extras, ""), + headerHash, + strings.Join(j.Nameservers, ""), + strings.Join(j.Questions, "")) + return hash +} + +func NewJSQuery(query *dns.Msg, clientIP string) *JSQuery { + answers := []string{} + extras := []string{} + nameservers := []string{} + questions := []string{} + + header := &JSQueryHeader{ + AuthenticatedData: query.MsgHdr.AuthenticatedData, + Authoritative: query.MsgHdr.Authoritative, + CheckingDisabled: query.MsgHdr.CheckingDisabled, + Id: query.MsgHdr.Id, + Opcode: query.MsgHdr.Opcode, + Rcode: query.MsgHdr.Rcode, + RecursionAvailable: query.MsgHdr.RecursionAvailable, + RecursionDesired: query.MsgHdr.RecursionDesired, + Response: query.MsgHdr.Response, + Truncated: query.MsgHdr.Truncated, + Zero: query.MsgHdr.Zero, + } + + for _, rr := range query.Answer { + answers = append(answers, rr.String()) + } + for _, rr := range query.Extra { + extras = append(extras, rr.String()) + } + for _, rr := range query.Ns { + nameservers = append(nameservers, rr.String()) + } + for _, q := range query.Question { + qType := dns.Type(q.Qtype).String() + qClass := dns.Class(q.Qclass).String() + questions = append(questions, fmt.Sprintf("%s\t%s\t%s", q.Name, qClass, qType)) + } + + clientMAC := "" + clientAlias := "" + if endpoint := session.I.Lan.GetByIp(clientIP); endpoint != nil { + clientMAC = endpoint.HwAddress + clientAlias = endpoint.Alias + } + client := map[string]string{"IP": clientIP, "MAC": clientMAC, "Alias": clientAlias} + + jsquery := &JSQuery{ + Answers: answers, + Client: client, + Compress: query.Compress, + Extras: extras, + Header: header, + Nameservers: nameservers, + Questions: questions, + } + jsquery.UpdateHash() + + return jsquery +} + +func stringToClass(s string) (uint16, error) { + for i, dnsClass := range dns.ClassToString { + if s == dnsClass { + return i, nil + } + } + return 0, fmt.Errorf("unkown DNS class (got %s)", s) +} + +func stringToType(s string) (uint16, error) { + for i, dnsType := range dns.TypeToString { + if s == dnsType { + return i, nil + } + } + return 0, fmt.Errorf("unkown DNS type (got %s)", s) +} + +func (j *JSQuery) ToQuery() *dns.Msg { + var answers []dns.RR + var extras []dns.RR + var nameservers []dns.RR + var questions []dns.Question + + for _, s := range j.Answers { + rr, err := dns.NewRR(s) + if err != nil { + log.Error("error parsing DNS answer resource record: %s", err.Error()) + return nil + } else { + answers = append(answers, rr) + } + } + for _, s := range j.Extras { + rr, err := dns.NewRR(s) + if err != nil { + log.Error("error parsing DNS extra resource record: %s", err.Error()) + return nil + } else { + extras = append(extras, rr) + } + } + for _, s := range j.Nameservers { + rr, err := dns.NewRR(s) + if err != nil { + log.Error("error parsing DNS nameserver resource record: %s", err.Error()) + return nil + } else { + nameservers = append(nameservers, rr) + } + } + + for _, s := range j.Questions { + qStripped := stripWhiteSpaceRegexp.FindStringSubmatch(s) + qParts := whiteSpaceRegexp.Split(qStripped[1], -1) + + if len(qParts) != 3 { + log.Error("invalid DNS question format: (got %s)", s) + return nil + } + + qName := dns.Fqdn(qParts[0]) + qClass, err := stringToClass(qParts[1]) + if err != nil { + log.Error("error parsing DNS question class: %s", err.Error()) + return nil + } + qType, err := stringToType(qParts[2]) + if err != nil { + log.Error("error parsing DNS question type: %s", err.Error()) + return nil + } + + questions = append(questions, dns.Question{qName, qType, qClass}) + } + + query := &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: j.Header.Id, + Response: j.Header.Response, + Opcode: j.Header.Opcode, + Authoritative: j.Header.Authoritative, + Truncated: j.Header.Truncated, + RecursionDesired: j.Header.RecursionDesired, + RecursionAvailable: j.Header.RecursionAvailable, + Zero: j.Header.Zero, + AuthenticatedData: j.Header.AuthenticatedData, + CheckingDisabled: j.Header.CheckingDisabled, + Rcode: j.Header.Rcode, + }, + Compress: j.Compress, + Question: questions, + Answer: answers, + Ns: nameservers, + Extra: extras, + } + + return query +} + +func (j *JSQuery) UpdateHash() { + j.refHash = j.NewHash() +} + +func (j *JSQuery) WasModified() bool { + // check if any of the fields has been changed + return j.NewHash() != j.refHash +} diff --git a/modules/dns_proxy/dns_proxy_script.go b/modules/dns_proxy/dns_proxy_script.go new file mode 100644 index 00000000..aa2c9d5f --- /dev/null +++ b/modules/dns_proxy/dns_proxy_script.go @@ -0,0 +1,99 @@ +package dns_proxy + +import ( + "github.com/bettercap/bettercap/v2/log" + "github.com/bettercap/bettercap/v2/session" + "github.com/evilsocket/islazy/plugin" + + "github.com/miekg/dns" + + "github.com/robertkrimen/otto" +) + +type DnsProxyScript struct { + *plugin.Plugin + + doOnRequest bool + doOnResponse bool + doOnCommand bool +} + +func LoadDnsProxyScript(path string, sess *session.Session) (err error, s *DnsProxyScript) { + log.Debug("loading proxy script %s ...", path) + + plug, err := plugin.Load(path) + if err != nil { + return + } + + // define session pointer + if err = plug.Set("env", sess.Env.Data); err != nil { + log.Error("Error while defining environment: %+v", err) + return + } + + // run onLoad if defined + if plug.HasFunc("onLoad") { + if _, err = plug.Call("onLoad"); err != nil { + log.Error("Error while executing onLoad callback: %s", "\nTraceback:\n "+err.(*otto.Error).String()) + return + } + } + + s = &DnsProxyScript{ + Plugin: plug, + doOnRequest: plug.HasFunc("onRequest"), + doOnResponse: plug.HasFunc("onResponse"), + doOnCommand: plug.HasFunc("onCommand"), + } + return +} + +func (s *DnsProxyScript) OnRequest(req *dns.Msg, clientIP string) (jsreq, jsres *JSQuery) { + if s.doOnRequest { + jsreq := NewJSQuery(req, clientIP) + jsres := NewJSQuery(req, clientIP) + + if _, err := s.Call("onRequest", jsreq, jsres); err != nil { + log.Error("%s", err) + return nil, nil + } else if jsreq.WasModified() { + jsreq.UpdateHash() + return jsreq, nil + } else if jsres.WasModified() { + jsres.UpdateHash() + return nil, jsres + } + } + + return nil, nil +} + +func (s *DnsProxyScript) OnResponse(req, res *dns.Msg, clientIP string) (jsreq, jsres *JSQuery) { + if s.doOnResponse { + jsreq := NewJSQuery(req, clientIP) + jsres := NewJSQuery(res, clientIP) + + if _, err := s.Call("onResponse", jsreq, jsres); err != nil { + log.Error("%s", err) + return nil, nil + } else if jsres.WasModified() { + jsres.UpdateHash() + return nil, jsres + } + } + + return nil, nil +} + +func (s *DnsProxyScript) OnCommand(cmd string) bool { + if s.doOnCommand { + if ret, err := s.Call("onCommand", cmd); err != nil { + log.Error("Error while executing onCommand callback: %+v", err) + return false + } else if v, ok := ret.(bool); ok { + return v + } + } + return false +} diff --git a/modules/modules.go b/modules/modules.go index 8af00309..984cb95f 100644 --- a/modules/modules.go +++ b/modules/modules.go @@ -9,6 +9,7 @@ import ( "github.com/bettercap/bettercap/v2/modules/can" "github.com/bettercap/bettercap/v2/modules/caplets" "github.com/bettercap/bettercap/v2/modules/dhcp6_spoof" + "github.com/bettercap/bettercap/v2/modules/dns_proxy" "github.com/bettercap/bettercap/v2/modules/dns_spoof" "github.com/bettercap/bettercap/v2/modules/events_stream" "github.com/bettercap/bettercap/v2/modules/gps" @@ -45,6 +46,7 @@ func LoadModules(sess *session.Session) { sess.Register(can.NewCanModule(sess)) sess.Register(dhcp6_spoof.NewDHCP6Spoofer(sess)) sess.Register(net_recon.NewDiscovery(sess)) + sess.Register(dns_proxy.NewDnsProxy(sess)) sess.Register(dns_spoof.NewDNSSpoofer(sess)) sess.Register(events_stream.NewEventsStream(sess)) sess.Register(gps.NewGPS(sess)) From 43f1013f0d3a5c7a24e18e9415e34c34b0a0711e Mon Sep 17 00:00:00 2001 From: buffermet <29265684+buffermet@users.noreply.github.com> Date: Wed, 9 Oct 2024 13:47:21 +0200 Subject: [PATCH 02/10] Add TLS support for DNS proxy, implement backwards compatible DNS record conversion. --- modules/dns_proxy/dns_proxy.go | 79 +- modules/dns_proxy/dns_proxy_base.go | 43 +- modules/dns_proxy/dns_proxy_base_filters.go | 73 +- modules/dns_proxy/dns_proxy_js_query.go | 196 ++-- modules/dns_proxy/dns_proxy_js_record.go | 1014 +++++++++++++++++ .../dns_proxy/dns_proxy_js_record_edns0.go | 208 ++++ modules/dns_proxy/dns_proxy_js_record_svcb.go | 127 +++ tls/cert.go | 8 + 8 files changed, 1603 insertions(+), 145 deletions(-) create mode 100644 modules/dns_proxy/dns_proxy_js_record.go create mode 100644 modules/dns_proxy/dns_proxy_js_record_edns0.go create mode 100644 modules/dns_proxy/dns_proxy_js_record_svcb.go diff --git a/modules/dns_proxy/dns_proxy.go b/modules/dns_proxy/dns_proxy.go index e737504b..f27cde2c 100644 --- a/modules/dns_proxy/dns_proxy.go +++ b/modules/dns_proxy/dns_proxy.go @@ -2,6 +2,10 @@ package dns_proxy import ( "github.com/bettercap/bettercap/v2/session" + "github.com/bettercap/bettercap/v2/tls" + + "github.com/evilsocket/islazy/fs" + "github.com/evilsocket/islazy/str" ) type DnsProxy struct { @@ -22,6 +26,10 @@ func (mod *DnsProxy) Configure() error { var netProtocol string var proxyPort int var scriptPath string + var certFile string + var keyFile string + var whitelist string + var blacklist string if mod.Running() { return session.ErrAlreadyStarted(mod.Name()) @@ -29,21 +37,56 @@ func (mod *DnsProxy) Configure() error { return err } else if err, address = mod.StringParam("dns.proxy.address"); err != nil { return err + } else if err, certFile = mod.StringParam("dns.proxy.certificate"); err != nil { + return err + } else if certFile, err = fs.Expand(certFile); err != nil { + return err + } else if err, keyFile = mod.StringParam("dns.proxy.key"); err != nil { + return err + } else if keyFile, err = fs.Expand(keyFile); err != nil { + return err } else if err, nameserver = mod.StringParam("dns.proxy.nameserver"); err != nil { return err - } else if err, netProtocol = mod.StringParam("dns.proxy.networkprotocol"); err != nil { - return err } else if err, proxyPort = mod.IntParam("dns.proxy.port"); err != nil { return err + } else if err, netProtocol = mod.StringParam("dns.proxy.protocol"); err != nil { + return err } else if err, doRedirect = mod.BoolParam("dns.proxy.redirect"); err != nil { return err } else if err, scriptPath = mod.StringParam("dns.proxy.script"); err != nil { return err + } else if err, blacklist = mod.StringParam("dns.proxy.blacklist"); err != nil { + return err + } else if err, whitelist = mod.StringParam("dns.proxy.whitelist"); err != nil { + return err } - error := mod.proxy.Configure(address, dnsPort, doRedirect, nameserver, netProtocol, proxyPort, scriptPath) + mod.proxy.Blacklist = str.Comma(blacklist) + mod.proxy.Whitelist = str.Comma(whitelist) - return error + if netProtocol == "tcp-tls" { + if !fs.Exists(certFile) || !fs.Exists(keyFile) { + cfg, err := tls.CertConfigFromModule("dns.proxy", mod.SessionModule) + if err != nil { + return err + } + + mod.Debug("%+v", cfg) + mod.Info("generating proxy certification authority TLS key to %s", keyFile) + mod.Info("generating proxy certification authority TLS certificate to %s", certFile) + if err := tls.Generate(cfg, certFile, keyFile, true); err != nil { + return err + } + } else { + mod.Info("loading proxy certification authority TLS key from %s", keyFile) + mod.Info("loading proxy certification authority TLS certificate from %s", certFile) + } + } + + err = mod.proxy.Configure(address, dnsPort, doRedirect, nameserver, netProtocol, + proxyPort, scriptPath, certFile, keyFile) + + return err } func (mod *DnsProxy) Description() string { @@ -69,24 +112,42 @@ func NewDnsProxy(s *session.Session) *DnsProxy { session.IPv4Validator, "Address to bind the DNS proxy to.")) + mod.AddParam(session.NewStringParameter("dns.proxy.blacklist", "", "", + "Comma separated list of hostnames to skip while proxying (wildcard expressions can be used).")) + + mod.AddParam(session.NewStringParameter("dns.proxy.whitelist", "", "", + "Comma separated list of hostnames to proxy if the blacklist is used (wildcard expressions can be used).")) + mod.AddParam(session.NewStringParameter("dns.proxy.nameserver", "1.1.1.1", session.IPv4Validator, "DNS resolver address.")) - mod.AddParam(session.NewStringParameter("dns.proxy.networkprotocol", - "udp", - "^(udp|tcp|tcp-tls)$", - "Network protocol for the DNS proxy server to use. Accepted values: udp, tcp, tcp-tls")) - mod.AddParam(session.NewIntParameter("dns.proxy.port", "8053", "Port to bind the DNS proxy to.")) + mod.AddParam(session.NewStringParameter("dns.proxy.protocol", + "udp", + "^(udp|tcp|tcp-tls)$", + "Network protocol for the DNS proxy server to use. Accepted values: udp, tcp, tcp-tls")) + mod.AddParam(session.NewBoolParameter("dns.proxy.redirect", "true", "Enable or disable port redirection with iptables.")) + mod.AddParam(session.NewStringParameter("dns.proxy.certificate", + "~/.bettercap-ca.cert.pem", + "", + "DNS proxy certification authority TLS certificate file.")) + + mod.AddParam(session.NewStringParameter("dns.proxy.key", + "~/.bettercap-ca.key.pem", + "", + "DNS proxy certification authority TLS key file.")) + + tls.CertConfigToModule("dns.proxy", &mod.SessionModule, tls.DefaultCloudflareDNSConfig) + mod.AddParam(session.NewStringParameter("dns.proxy.script", "", "", diff --git a/modules/dns_proxy/dns_proxy_base.go b/modules/dns_proxy/dns_proxy_base.go index 1c619e95..b9652237 100644 --- a/modules/dns_proxy/dns_proxy_base.go +++ b/modules/dns_proxy/dns_proxy_base.go @@ -2,7 +2,10 @@ package dns_proxy import ( "context" + "crypto/tls" + "crypto/x509" "fmt" + "io/ioutil" "strings" "time" @@ -20,13 +23,17 @@ const ( ) type DNSProxy struct { - Address string Name string + Address string + Server *dns.Server + Redirection *firewall.Redirection Nameserver string NetProtocol string - Redirection *firewall.Redirection Script *DnsProxyScript - Server *dns.Server + CertFile string + KeyFile string + Blacklist []string + Whitelist []string Sess *session.Session doRedirect bool @@ -34,11 +41,13 @@ type DNSProxy struct { tag string } -func (p *DNSProxy) Configure(address string, dnsPort int, doRedirect bool, nameserver string, netProtocol string, proxyPort int, scriptPath string) error { +func (p *DNSProxy) Configure(address string, dnsPort int, doRedirect bool, nameserver string, netProtocol string, proxyPort int, scriptPath string, certFile string, keyFile string) error { var err error p.Address = address p.doRedirect = doRedirect + p.CertFile = certFile + p.KeyFile = keyFile if scriptPath != "" { if err, p.Script = LoadDnsProxyScript(scriptPath, p.Sess); err != nil { @@ -77,9 +86,10 @@ func (p *DNSProxy) Configure(address string, dnsPort int, doRedirect bool, names } res = p.onResponseFilter(req, res, clientIP) if res == nil { - p.Error("response filter returned nil") + p.Debug("response is nil") m.SetRcode(req, dns.RcodeServerFailure) w.WriteMsg(m) + return } else { if err := w.WriteMsg(res); err != nil { p.Error("Error writing response: %s", err) @@ -98,14 +108,35 @@ func (p *DNSProxy) Configure(address string, dnsPort int, doRedirect bool, names Handler: handler, } + if netProtocol == "tcp-tls" && p.CertFile != "" && p.KeyFile != "" { + rawCert, _ := ioutil.ReadFile(p.CertFile) + rawKey, _ := ioutil.ReadFile(p.KeyFile) + ourCa, err := tls.X509KeyPair(rawCert, rawKey) + if err != nil { + return err + } + + if ourCa.Leaf, err = x509.ParseCertificate(ourCa.Certificate[0]); err != nil { + return err + } + + p.Server.TLSConfig = &tls.Config{ + Certificates: []tls.Certificate{ourCa}, + } + } + if p.doRedirect { if !p.Sess.Firewall.IsForwardingEnabled() { p.Info("enabling forwarding.") p.Sess.Firewall.EnableForwarding(true) } + redirectProtocol := netProtocol + if redirectProtocol == "tcp-tls" { + redirectProtocol = "tcp" + } p.Redirection = firewall.NewRedirection(p.Sess.Interface.Name(), - netProtocol, + redirectProtocol, dnsPort, p.Address, proxyPort) diff --git a/modules/dns_proxy/dns_proxy_base_filters.go b/modules/dns_proxy/dns_proxy_base_filters.go index 5255b608..ad12db76 100644 --- a/modules/dns_proxy/dns_proxy_base_filters.go +++ b/modules/dns_proxy/dns_proxy_base_filters.go @@ -6,43 +6,60 @@ import ( "github.com/miekg/dns" ) -func shortenResourceRecords(records []string) []string { - shorterRecords := []string{} - for _, record := range records { - shorterRecord := strings.ReplaceAll(record, "\t", " ") - shorterRecords = append(shorterRecords, shorterRecord) +func questionsToStrings(qs []dns.Question) []string { + questions := []string{} + for _, q := range qs { + questions = append(questions, tabsToSpaces(q.String())) } - return shorterRecords + return questions } -func (p *DNSProxy) logRequestAction(j *JSQuery) { +func recordsToStrings(rrs []dns.RR) []string { + records := []string{} + for _, rr := range rrs { + records = append(records, tabsToSpaces(rr.String())) + } + return records +} + +func tabsToSpaces(s string) string { + return strings.ReplaceAll(s, "\t", " ") +} + +func (p *DNSProxy) logRequestAction(m *dns.Msg, clientIP string) { + var questions []string + for _, q := range m.Question { + questions = append(questions, tabsToSpaces(q.String())) + } p.Sess.Events.Add(p.Name+".spoofed-request", struct { Client string Questions []string }{ - j.Client["IP"], - shortenResourceRecords(j.Questions), + clientIP, + questions, }) } -func (p *DNSProxy) logResponseAction(j *JSQuery) { +func (p *DNSProxy) logResponseAction(m *dns.Msg, clientIP string) { p.Sess.Events.Add(p.Name+".spoofed-response", struct { client string - Extras []string Answers []string + Extras []string Nameservers []string Questions []string }{ - j.Client["IP"], - shortenResourceRecords(j.Extras), - shortenResourceRecords(j.Answers), - shortenResourceRecords(j.Nameservers), - shortenResourceRecords(j.Questions), + clientIP, + recordsToStrings(m.Answer), + recordsToStrings(m.Extra), + recordsToStrings(m.Ns), + questionsToStrings(m.Question), }) } func (p *DNSProxy) onRequestFilter(query *dns.Msg, clientIP string) (req, res *dns.Msg) { - p.Debug("< %s %s", clientIP, query.Question) + p.Debug("< %s %s", + clientIP, + strings.Join(questionsToStrings(query.Question), ",")) // do we have a proxy script? if p.Script == nil { @@ -53,12 +70,14 @@ func (p *DNSProxy) onRequestFilter(query *dns.Msg, clientIP string) (req, res *d jsreq, jsres := p.Script.OnRequest(query, clientIP) if jsreq != nil { // the request has been changed by the script - p.logRequestAction(jsreq) - return jsreq.ToQuery(), nil + req := jsreq.ToQuery() + p.logRequestAction(req, clientIP) + return req, nil } else if jsres != nil { // a fake response has been returned by the script - p.logResponseAction(jsres) - return query, jsres.ToQuery() + res := jsres.ToQuery() + p.logResponseAction(res, clientIP) + return query, res } return query, nil @@ -70,15 +89,21 @@ func (p *DNSProxy) onResponseFilter(req, res *dns.Msg, clientIP string) *dns.Msg return nil } - p.Debug("> %s %s", clientIP, res.Answer) + p.Debug("> %s %s [%s] [%s] [%s]", + clientIP, + strings.Join(questionsToStrings(res.Question), ","), + strings.Join(recordsToStrings(res.Answer), ","), + strings.Join(recordsToStrings(res.Extra), ","), + strings.Join(recordsToStrings(res.Ns), ",")) // do we have a proxy script? if p.Script != nil { _, jsres := p.Script.OnResponse(req, res, clientIP) if jsres != nil { // the response has been changed by the script - p.logResponseAction(jsres) - return jsres.ToQuery() + res := jsres.ToQuery() + p.logResponseAction(res, clientIP) + return res } } diff --git a/modules/dns_proxy/dns_proxy_js_query.go b/modules/dns_proxy/dns_proxy_js_query.go index a13a6ef6..7db5118a 100644 --- a/modules/dns_proxy/dns_proxy_js_query.go +++ b/modules/dns_proxy/dns_proxy_js_query.go @@ -1,9 +1,8 @@ package dns_proxy import ( + "encoding/json" "fmt" - "regexp" - "strings" "github.com/bettercap/bettercap/v2/log" "github.com/bettercap/bettercap/v2/session" @@ -11,17 +10,14 @@ import ( "github.com/miekg/dns" ) -var whiteSpaceRegexp = regexp.MustCompile(`\s+`) -var stripWhiteSpaceRegexp = regexp.MustCompile(`^\s*(.*?)\s*$`) - type JSQuery struct { - Answers []string + Answers []map[string]interface{} Client map[string]string - Compress bool `json:"-"` - Extras []string - Header *JSQueryHeader - Nameservers []string - Questions []string + Compress bool + Extras []map[string]interface{} + Header JSQueryHeader + Nameservers []map[string]interface{} + Questions []map[string]interface{} refHash string } @@ -41,6 +37,11 @@ type JSQueryHeader struct { } func (j *JSQuery) NewHash() string { + answers, _ := json.Marshal(j.Answers) + extras, _ := json.Marshal(j.Extras) + nameservers, _ := json.Marshal(j.Nameservers) + questions, _ := json.Marshal(j.Questions) + headerHash := fmt.Sprintf("%t.%t.%t.%d.%d.%d.%t.%t.%t.%t.%t", j.Header.AuthenticatedData, j.Header.Authoritative, @@ -53,50 +54,58 @@ func (j *JSQuery) NewHash() string { j.Header.Response, j.Header.Truncated, j.Header.Zero) + hash := fmt.Sprintf("%s.%s.%t.%s.%s.%s.%s", - strings.Join(j.Answers, ""), + answers, j.Client["IP"], j.Compress, - strings.Join(j.Extras, ""), + extras, headerHash, - strings.Join(j.Nameservers, ""), - strings.Join(j.Questions, "")) + nameservers, + questions) + return hash } -func NewJSQuery(query *dns.Msg, clientIP string) *JSQuery { - answers := []string{} - extras := []string{} - nameservers := []string{} - questions := []string{} +func NewJSQuery(query *dns.Msg, clientIP string) (jsQuery *JSQuery) { + answers := make([]map[string]interface{}, len(query.Answer)) + extras := make([]map[string]interface{}, len(query.Extra)) + nameservers := make([]map[string]interface{}, len(query.Ns)) + questions := make([]map[string]interface{}, len(query.Question)) - header := &JSQueryHeader{ - AuthenticatedData: query.MsgHdr.AuthenticatedData, - Authoritative: query.MsgHdr.Authoritative, - CheckingDisabled: query.MsgHdr.CheckingDisabled, - Id: query.MsgHdr.Id, - Opcode: query.MsgHdr.Opcode, - Rcode: query.MsgHdr.Rcode, - RecursionAvailable: query.MsgHdr.RecursionAvailable, - RecursionDesired: query.MsgHdr.RecursionDesired, - Response: query.MsgHdr.Response, - Truncated: query.MsgHdr.Truncated, - Zero: query.MsgHdr.Zero, + for i, rr := range query.Answer { + jsRecord, err := NewJSResourceRecord(rr) + if err != nil { + log.Error(err.Error()) + continue + } + answers[i] = jsRecord } - for _, rr := range query.Answer { - answers = append(answers, rr.String()) + for i, rr := range query.Extra { + jsRecord, err := NewJSResourceRecord(rr) + if err != nil { + log.Error(err.Error()) + continue + } + extras[i] = jsRecord } - for _, rr := range query.Extra { - extras = append(extras, rr.String()) + + for i, rr := range query.Ns { + jsRecord, err := NewJSResourceRecord(rr) + if err != nil { + log.Error(err.Error()) + continue + } + nameservers[i] = jsRecord } - for _, rr := range query.Ns { - nameservers = append(nameservers, rr.String()) - } - for _, q := range query.Question { - qType := dns.Type(q.Qtype).String() - qClass := dns.Class(q.Qclass).String() - questions = append(questions, fmt.Sprintf("%s\t%s\t%s", q.Name, qClass, qType)) + + for i, question := range query.Question { + questions[i] = map[string]interface{}{ + "Name": question.Name, + "Qtype": question.Qtype, + "Qclass": question.Qclass, + } } clientMAC := "" @@ -108,11 +117,23 @@ func NewJSQuery(query *dns.Msg, clientIP string) *JSQuery { client := map[string]string{"IP": clientIP, "MAC": clientMAC, "Alias": clientAlias} jsquery := &JSQuery{ - Answers: answers, - Client: client, - Compress: query.Compress, - Extras: extras, - Header: header, + Answers: answers, + Client: client, + Compress: query.Compress, + Extras: extras, + Header: JSQueryHeader{ + AuthenticatedData: query.MsgHdr.AuthenticatedData, + Authoritative: query.MsgHdr.Authoritative, + CheckingDisabled: query.MsgHdr.CheckingDisabled, + Id: query.MsgHdr.Id, + Opcode: query.MsgHdr.Opcode, + Rcode: query.MsgHdr.Rcode, + RecursionAvailable: query.MsgHdr.RecursionAvailable, + RecursionDesired: query.MsgHdr.RecursionDesired, + Response: query.MsgHdr.Response, + Truncated: query.MsgHdr.Truncated, + Zero: query.MsgHdr.Zero, + }, Nameservers: nameservers, Questions: questions, } @@ -121,80 +142,43 @@ func NewJSQuery(query *dns.Msg, clientIP string) *JSQuery { return jsquery } -func stringToClass(s string) (uint16, error) { - for i, dnsClass := range dns.ClassToString { - if s == dnsClass { - return i, nil - } - } - return 0, fmt.Errorf("unkown DNS class (got %s)", s) -} - -func stringToType(s string) (uint16, error) { - for i, dnsType := range dns.TypeToString { - if s == dnsType { - return i, nil - } - } - return 0, fmt.Errorf("unkown DNS type (got %s)", s) -} - func (j *JSQuery) ToQuery() *dns.Msg { var answers []dns.RR var extras []dns.RR var nameservers []dns.RR var questions []dns.Question - for _, s := range j.Answers { - rr, err := dns.NewRR(s) + for _, jsRR := range j.Answers { + rr, err := ToRR(jsRR) if err != nil { - log.Error("error parsing DNS answer resource record: %s", err.Error()) - return nil - } else { - answers = append(answers, rr) + log.Error(err.Error()) + continue } + answers = append(answers, rr) } - for _, s := range j.Extras { - rr, err := dns.NewRR(s) + for _, jsRR := range j.Extras { + rr, err := ToRR(jsRR) if err != nil { - log.Error("error parsing DNS extra resource record: %s", err.Error()) - return nil - } else { - extras = append(extras, rr) + log.Error(err.Error()) + continue } + extras = append(extras, rr) } - for _, s := range j.Nameservers { - rr, err := dns.NewRR(s) + for _, jsRR := range j.Nameservers { + rr, err := ToRR(jsRR) if err != nil { - log.Error("error parsing DNS nameserver resource record: %s", err.Error()) - return nil - } else { - nameservers = append(nameservers, rr) + log.Error(err.Error()) + continue } + nameservers = append(nameservers, rr) } - for _, s := range j.Questions { - qStripped := stripWhiteSpaceRegexp.FindStringSubmatch(s) - qParts := whiteSpaceRegexp.Split(qStripped[1], -1) - - if len(qParts) != 3 { - log.Error("invalid DNS question format: (got %s)", s) - return nil - } - - qName := dns.Fqdn(qParts[0]) - qClass, err := stringToClass(qParts[1]) - if err != nil { - log.Error("error parsing DNS question class: %s", err.Error()) - return nil - } - qType, err := stringToType(qParts[2]) - if err != nil { - log.Error("error parsing DNS question type: %s", err.Error()) - return nil - } - - questions = append(questions, dns.Question{qName, qType, qClass}) + for _, jsQ := range j.Questions { + questions = append(questions, dns.Question{ + Name: jsPropToString(jsQ, "Name"), + Qtype: jsPropToUint16(jsQ, "Qtype"), + Qclass: jsPropToUint16(jsQ, "Qclass"), + }) } query := &dns.Msg{ diff --git a/modules/dns_proxy/dns_proxy_js_record.go b/modules/dns_proxy/dns_proxy_js_record.go new file mode 100644 index 00000000..647267e2 --- /dev/null +++ b/modules/dns_proxy/dns_proxy_js_record.go @@ -0,0 +1,1014 @@ +package dns_proxy + +import ( + "fmt" + "net" + + "github.com/bettercap/bettercap/v2/log" + "github.com/miekg/dns" +) + +func jsPropToMap(obj map[string]interface{}, key string) map[string]interface{} { + if v, ok := obj[key].(map[string]interface{}); ok { + return v + } + return map[string]interface{}{} +} + +func jsPropToMapArray(obj map[string]interface{}, key string) []map[string]interface{} { + if v, ok := obj[key].([]map[string]interface{}); ok { + return v + } + return []map[string]interface{}{} +} + +func jsPropToString(obj map[string]interface{}, key string) string { + if v, ok := obj[key].(string); ok { + return v + } + return "" +} + +func jsPropToStringArray(obj map[string]interface{}, key string) []string { + if v, ok := obj[key].([]string); ok { + return v + } + return []string{} +} + +func jsPropToUint8(obj map[string]interface{}, key string) uint8 { + if v, ok := obj[key].(uint8); ok { + return v + } + return 0 +} + +func jsPropToUint8Array(obj map[string]interface{}, key string) []uint8 { + if v, ok := obj[key].([]uint8); ok { + return v + } + return []uint8{} +} + +func jsPropToUint16(obj map[string]interface{}, key string) uint16 { + if v, ok := obj[key].(uint16); ok { + return v + } + return 0 +} + +func jsPropToUint16Array(obj map[string]interface{}, key string) []uint16 { + if v, ok := obj[key].([]uint16); ok { + return v + } + return []uint16{} +} + +func jsPropToUint32(obj map[string]interface{}, key string) uint32 { + if v, ok := obj[key].(uint32); ok { + return v + } + return 0 +} + +func jsPropToUint64(obj map[string]interface{}, key string) uint64 { + if v, ok := obj[key].(uint64); ok { + return v + } + return 0 +} + +func NewJSResourceRecord(rr dns.RR) (jsRecord map[string]interface{}, err error) { + header := rr.Header() + + jsRecord = map[string]interface{}{ + "Header": map[string]interface{}{ + "Class": header.Class, + "Name": header.Name, + "Rrtype": header.Rrtype, + "Ttl": header.Ttl, + }, + } + + switch rr := rr.(type) { + case *dns.A: + jsRecord["A"] = rr.A.String() + case *dns.AAAA: + jsRecord["AAAA"] = rr.AAAA.String() + case *dns.APL: + jsPrefixes := make([]map[string]interface{}, len(rr.Prefixes)) + for i, v := range rr.Prefixes { + jsPrefixes[i] = map[string]interface{}{ + "Negation": v.Negation, + "Network": v.Network.String(), + } + } + jsRecord["Prefixes"] = jsPrefixes + case *dns.CNAME: + jsRecord["Target"] = rr.Target + case *dns.MB: + jsRecord["Mb"] = rr.Mb + case *dns.MD: + jsRecord["Md"] = rr.Md + case *dns.MF: + jsRecord["Mf"] = rr.Mf + case *dns.MG: + jsRecord["Mg"] = rr.Mg + case *dns.MR: + jsRecord["Mr"] = rr.Mr + case *dns.MX: + jsRecord["Mx"] = rr.Mx + jsRecord["Preference"] = rr.Preference + case *dns.NULL: + jsRecord["Data"] = rr.Data + case *dns.SOA: + jsRecord["Expire"] = rr.Expire + jsRecord["Minttl"] = rr.Minttl + jsRecord["Ns"] = rr.Ns + jsRecord["Refresh"] = rr.Refresh + jsRecord["Retry"] = rr.Retry + jsRecord["Mbox"] = rr.Mbox + jsRecord["Serial"] = rr.Serial + case *dns.TXT: + jsRecord["Txt"] = rr.Txt + case *dns.SRV: + jsRecord["Port"] = rr.Port + jsRecord["Priority"] = rr.Priority + jsRecord["Target"] = rr.Target + jsRecord["Weight"] = rr.Weight + case *dns.PTR: + jsRecord["Ptr"] = rr.Ptr + case *dns.NS: + jsRecord["Ns"] = rr.Ns + case *dns.DNAME: + jsRecord["Target"] = rr.Target + case *dns.AFSDB: + jsRecord["Subtype"] = rr.Subtype + jsRecord["Hostname"] = rr.Hostname + case *dns.CAA: + jsRecord["Flag"] = rr.Flag + jsRecord["Tag"] = rr.Tag + jsRecord["Value"] = rr.Value + case *dns.HINFO: + jsRecord["Cpu"] = rr.Cpu + jsRecord["Os"] = rr.Os + case *dns.MINFO: + jsRecord["Email"] = rr.Email + jsRecord["Rmail"] = rr.Rmail + case *dns.ISDN: + jsRecord["Address"] = rr.Address + jsRecord["SubAddress"] = rr.SubAddress + case *dns.KX: + jsRecord["Exchanger"] = rr.Exchanger + jsRecord["Preference"] = rr.Preference + case *dns.LOC: + jsRecord["Altitude"] = rr.Altitude + jsRecord["HorizPre"] = rr.HorizPre + jsRecord["Latitude"] = rr.Latitude + jsRecord["Longitude"] = rr.Longitude + jsRecord["Size"] = rr.Size + jsRecord["Version"] = rr.Version + jsRecord["VertPre"] = rr.VertPre + case *dns.SSHFP: + jsRecord["Algorithm"] = rr.Algorithm + jsRecord["FingerPrint"] = rr.FingerPrint + jsRecord["Type"] = rr.Type + case *dns.TLSA: + jsRecord["Certificate"] = rr.Certificate + jsRecord["MatchingType"] = rr.MatchingType + jsRecord["Selector"] = rr.Selector + jsRecord["Usage"] = rr.Usage + case *dns.CERT: + jsRecord["Algorithm"] = rr.Algorithm + jsRecord["Certificate"] = rr.Certificate + jsRecord["KeyTag"] = rr.KeyTag + jsRecord["Type"] = rr.Type + case *dns.DS: + jsRecord["Algorithm"] = rr.Algorithm + jsRecord["Digest"] = rr.Digest + jsRecord["DigestType"] = rr.DigestType + jsRecord["KeyTag"] = rr.KeyTag + case *dns.NAPTR: + jsRecord["Order"] = rr.Order + jsRecord["Preference"] = rr.Preference + jsRecord["Flags"] = rr.Flags + jsRecord["Service"] = rr.Service + jsRecord["Regexp"] = rr.Regexp + jsRecord["Replacement"] = rr.Replacement + case *dns.RRSIG: + jsRecord["Algorithm"] = rr.Algorithm + jsRecord["Expiration"] = rr.Expiration + jsRecord["Inception"] = rr.Inception + jsRecord["KeyTag"] = rr.KeyTag + jsRecord["Labels"] = rr.Labels + jsRecord["OrigTtl"] = rr.OrigTtl + jsRecord["Signature"] = rr.Signature + jsRecord["SignerName"] = rr.SignerName + jsRecord["TypeCovered"] = rr.TypeCovered + case *dns.NSEC: + jsRecord["NextDomain"] = rr.NextDomain + jsRecord["TypeBitMap"] = rr.TypeBitMap + case *dns.NSEC3: + jsRecord["Flags"] = rr.Flags + jsRecord["Hash"] = rr.Hash + jsRecord["HashLength"] = rr.HashLength + jsRecord["Iterations"] = rr.Iterations + jsRecord["NextDomain"] = rr.NextDomain + jsRecord["Salt"] = rr.Salt + jsRecord["SaltLength"] = rr.SaltLength + jsRecord["TypeBitMap"] = rr.TypeBitMap + case *dns.NSEC3PARAM: + jsRecord["Flags"] = rr.Flags + jsRecord["Hash"] = rr.Hash + jsRecord["Iterations"] = rr.Iterations + jsRecord["Salt"] = rr.Salt + jsRecord["SaltLength"] = rr.SaltLength + case *dns.TKEY: + jsRecord["Algorithm"] = rr.Algorithm + jsRecord["Error"] = rr.Error + jsRecord["Expiration"] = rr.Expiration + jsRecord["Inception"] = rr.Inception + jsRecord["Key"] = rr.Key + jsRecord["KeySize"] = rr.KeySize + jsRecord["Mode"] = rr.Mode + jsRecord["OtherData"] = rr.OtherData + jsRecord["OtherLen"] = rr.OtherLen + case *dns.TSIG: + jsRecord["Algorithm"] = rr.Algorithm + jsRecord["Error"] = rr.Error + jsRecord["Fudge"] = rr.Fudge + jsRecord["MACSize"] = rr.MACSize + jsRecord["MAC"] = rr.MAC + jsRecord["OrigId"] = rr.OrigId + jsRecord["OtherData"] = rr.OtherData + jsRecord["OtherLen"] = rr.OtherLen + jsRecord["TimeSigned"] = rr.TimeSigned + case *dns.IPSECKEY: + jsRecord["Algorithm"] = rr.Algorithm + jsRecord["GatewayAddr"] = rr.GatewayAddr.String() + jsRecord["GatewayHost"] = rr.GatewayHost + jsRecord["GatewayType"] = rr.GatewayType + jsRecord["Precedence"] = rr.Precedence + jsRecord["PublicKey"] = rr.PublicKey + case *dns.KEY: + jsRecord["Flags"] = rr.Flags + jsRecord["Protocol"] = rr.Protocol + jsRecord["Algorithm"] = rr.Algorithm + jsRecord["PublicKey"] = rr.PublicKey + case *dns.CDS: + jsRecord["KeyTag"] = rr.KeyTag + jsRecord["Algorithm"] = rr.Algorithm + jsRecord["DigestType"] = rr.DigestType + jsRecord["Digest"] = rr.Digest + case *dns.CDNSKEY: + jsRecord["Algorithm"] = rr.Algorithm + jsRecord["Flags"] = rr.Flags + jsRecord["Protocol"] = rr.Protocol + jsRecord["PublicKey"] = rr.PublicKey + case *dns.NID: + jsRecord["NodeID"] = rr.NodeID + jsRecord["Preference"] = rr.Preference + case *dns.L32: + jsRecord["Locator32"] = rr.Locator32.String() + jsRecord["Preference"] = rr.Preference + case *dns.L64: + jsRecord["Locator64"] = rr.Locator64 + jsRecord["Preference"] = rr.Preference + case *dns.LP: + jsRecord["Fqdn"] = rr.Fqdn + jsRecord["Preference"] = rr.Preference + case *dns.GPOS: + jsRecord["Altitude"] = rr.Altitude + jsRecord["Latitude"] = rr.Latitude + jsRecord["Longitude"] = rr.Longitude + case *dns.RP: + jsRecord["Mbox"] = rr.Mbox + jsRecord["Txt"] = rr.Txt + case *dns.RKEY: + jsRecord["Algorithm"] = rr.Algorithm + jsRecord["Flags"] = rr.Flags + jsRecord["Protocol"] = rr.Protocol + jsRecord["PublicKey"] = rr.PublicKey + case *dns.SMIMEA: + jsRecord["Certificate"] = rr.Certificate + jsRecord["MatchingType"] = rr.MatchingType + jsRecord["Selector"] = rr.Selector + jsRecord["Usage"] = rr.Usage + case *dns.AMTRELAY: + jsRecord["GatewayAddr"] = rr.GatewayAddr.String() + jsRecord["GatewayHost"] = rr.GatewayHost + jsRecord["GatewayType"] = rr.GatewayType + jsRecord["Precedence"] = rr.Precedence + case *dns.AVC: + jsRecord["Txt"] = rr.Txt + case *dns.URI: + jsRecord["Priority"] = rr.Priority + jsRecord["Weight"] = rr.Weight + jsRecord["Target"] = rr.Target + case *dns.EUI48: + jsRecord["Address"] = rr.Address + case *dns.EUI64: + jsRecord["Address"] = rr.Address + case *dns.GID: + jsRecord["Gid"] = rr.Gid + case *dns.UID: + jsRecord["Uid"] = rr.Uid + case *dns.UINFO: + jsRecord["Uinfo"] = rr.Uinfo + case *dns.SPF: + jsRecord["Txt"] = rr.Txt + case *dns.HTTPS: + jsRecord["Priority"] = rr.Priority + jsRecord["Target"] = rr.Target + kvs := rr.Value + var jsKvs []map[string]interface{} + for _, kv := range kvs { + jsKv, err := NewJSSVCBKeyValue(kv) + if err != nil { + log.Error(err.Error()) + continue + } + jsKvs = append(jsKvs, jsKv) + } + jsRecord["Value"] = jsKvs + case *dns.SVCB: + jsRecord["Priority"] = rr.Priority + jsRecord["Target"] = rr.Target + kvs := rr.Value + jsKvs := make([]map[string]interface{}, len(kvs)) + for i, kv := range kvs { + jsKv, err := NewJSSVCBKeyValue(kv) + if err != nil { + log.Error(err.Error()) + continue + } + jsKvs[i] = jsKv + } + jsRecord["Value"] = jsKvs + case *dns.ZONEMD: + jsRecord["Digest"] = rr.Digest + jsRecord["Hash"] = rr.Hash + jsRecord["Scheme"] = rr.Scheme + jsRecord["Serial"] = rr.Serial + case *dns.CSYNC: + jsRecord["Flags"] = rr.Flags + jsRecord["Serial"] = rr.Serial + jsRecord["TypeBitMap"] = rr.TypeBitMap + case *dns.OPENPGPKEY: + jsRecord["PublicKey"] = rr.PublicKey + case *dns.TALINK: + jsRecord["NextName"] = rr.NextName + jsRecord["PreviousName"] = rr.PreviousName + case *dns.NINFO: + jsRecord["ZSData"] = rr.ZSData + case *dns.DHCID: + jsRecord["Digest"] = rr.Digest + case *dns.DNSKEY: + jsRecord["Flags"] = rr.Flags + jsRecord["Protocol"] = rr.Protocol + jsRecord["Algorithm"] = rr.Algorithm + jsRecord["PublicKey"] = rr.PublicKey + case *dns.HIP: + jsRecord["Hit"] = rr.Hit + jsRecord["HitLength"] = rr.HitLength + jsRecord["PublicKey"] = rr.PublicKey + jsRecord["PublicKeyAlgorithm"] = rr.PublicKeyAlgorithm + jsRecord["PublicKeyLength"] = rr.PublicKeyLength + jsRecord["RendezvousServers"] = rr.RendezvousServers + case *dns.OPT: + jsRecord["Option"] = rr.Option + case *dns.NIMLOC: + jsRecord["Locator"] = rr.Locator + case *dns.EID: + jsRecord["Endpoint"] = rr.Endpoint + case *dns.NXT: + jsRecord["NextDomain"] = rr.NextDomain + jsRecord["TypeBitMap"] = rr.TypeBitMap + case *dns.PX: + jsRecord["Mapx400"] = rr.Mapx400 + jsRecord["Map822"] = rr.Map822 + jsRecord["Preference"] = rr.Preference + case *dns.SIG: + jsRecord["Algorithm"] = rr.Algorithm + jsRecord["Expiration"] = rr.Expiration + jsRecord["Inception"] = rr.Inception + jsRecord["KeyTag"] = rr.KeyTag + jsRecord["Labels"] = rr.Labels + jsRecord["OrigTtl"] = rr.OrigTtl + jsRecord["Signature"] = rr.Signature + jsRecord["SignerName"] = rr.SignerName + jsRecord["TypeCovered"] = rr.TypeCovered + case *dns.RT: + jsRecord["Host"] = rr.Host + jsRecord["Preference"] = rr.Preference + case *dns.NSAPPTR: + jsRecord["Ptr"] = rr.Ptr + case *dns.X25: + jsRecord["PSDNAddress"] = rr.PSDNAddress + case *dns.RFC3597: + jsRecord["Rdata"] = rr.Rdata + // case *dns.ATMA: + // case *dns.WKS: + // case *dns.DOA: + // case *dns.SINK: + default: + if header.Rrtype == dns.TypeNone { + break + } + return nil, fmt.Errorf("error creating JSResourceRecord: unknown type: %d", header.Rrtype) + } + + return jsRecord, nil +} + +func ToRR(jsRecord map[string]interface{}) (rr dns.RR, err error) { + jsHeader := jsPropToMap(jsRecord, "Header") + + header := dns.RR_Header{ + Class: jsPropToUint16(jsHeader, "Class"), + Name: jsPropToString(jsHeader, "Name"), + Rrtype: jsPropToUint16(jsHeader, "Rrtype"), + Ttl: jsPropToUint32(jsHeader, "Ttl"), + } + + switch header.Rrtype { + case dns.TypeNone: + break + case dns.TypeA: + rr = &dns.A{ + Hdr: header, + A: net.ParseIP(jsPropToString(jsRecord, "A")), + } + case dns.TypeAAAA: + rr = &dns.AAAA{ + Hdr: header, + AAAA: net.ParseIP(jsPropToString(jsRecord, "AAAA")), + } + case dns.TypeAPL: + jsPrefixes := jsRecord["Prefixes"].([]map[string]interface{}) + prefixes := make([]dns.APLPrefix, len(jsPrefixes)) + for i, jsPrefix := range jsPrefixes { + jsNetwork := jsPrefix["Network"].(string) + _, network, err := net.ParseCIDR(jsNetwork) + if err != nil { + log.Error("error parsing CIDR: %s", jsNetwork) + continue + } + prefixes[i] = dns.APLPrefix{ + Negation: jsPrefix["Negation"].(bool), + Network: *network, + } + } + rr = &dns.APL{ + Hdr: header, + Prefixes: prefixes, + } + case dns.TypeCNAME: + rr = &dns.CNAME{ + Hdr: header, + Target: jsPropToString(jsRecord, "Target"), + } + case dns.TypeMB: + rr = &dns.MB{ + Hdr: header, + Mb: jsPropToString(jsRecord, "Mb"), + } + case dns.TypeMD: + rr = &dns.MD{ + Hdr: header, + Md: jsPropToString(jsRecord, "Md"), + } + case dns.TypeMF: + rr = &dns.MF{ + Hdr: header, + Mf: jsPropToString(jsRecord, "Mf"), + } + case dns.TypeMG: + rr = &dns.MG{ + Hdr: header, + Mg: jsPropToString(jsRecord, "Mg"), + } + case dns.TypeMR: + rr = &dns.MR{ + Hdr: header, + Mr: jsPropToString(jsRecord, "Mr"), + } + case dns.TypeMX: + rr = &dns.MX{ + Hdr: header, + Mx: jsPropToString(jsRecord, "Mx"), + Preference: jsPropToUint16(jsRecord, "Preference"), + } + case dns.TypeNULL: + rr = &dns.NULL{ + Hdr: header, + Data: jsPropToString(jsRecord, "Data"), + } + case dns.TypeSOA: + rr = &dns.SOA{ + Hdr: header, + Expire: jsPropToUint32(jsRecord, "Expire"), + Mbox: jsPropToString(jsRecord, "Mbox"), + Minttl: jsPropToUint32(jsRecord, "Minttl"), + Ns: jsPropToString(jsRecord, "Ns"), + Refresh: jsPropToUint32(jsRecord, "Refresh"), + Retry: jsPropToUint32(jsRecord, "Retry"), + Serial: jsPropToUint32(jsRecord, "Serial"), + } + case dns.TypeTXT: + rr = &dns.TXT{ + Hdr: header, + Txt: jsPropToStringArray(jsRecord, "Txt"), + } + case dns.TypeSRV: + rr = &dns.SRV{ + Hdr: header, + Port: jsPropToUint16(jsRecord, "Port"), + Priority: jsPropToUint16(jsRecord, "Priority"), + Target: jsPropToString(jsRecord, "Target"), + Weight: jsPropToUint16(jsRecord, "Weight"), + } + case dns.TypePTR: + rr = &dns.PTR{ + Hdr: header, + Ptr: jsPropToString(jsRecord, "Ptr"), + } + case dns.TypeNS: + rr = &dns.NS{ + Hdr: header, + Ns: jsPropToString(jsRecord, "Ns"), + } + case dns.TypeDNAME: + rr = &dns.DNAME{ + Hdr: header, + Target: jsPropToString(jsRecord, "Target"), + } + case dns.TypeAFSDB: + rr = &dns.AFSDB{ + Hdr: header, + Hostname: jsPropToString(jsRecord, "Hostname"), + Subtype: jsPropToUint16(jsRecord, "Subtype"), + } + case dns.TypeCAA: + rr = &dns.CAA{ + Hdr: header, + Flag: jsPropToUint8(jsRecord, "Flag"), + Tag: jsPropToString(jsRecord, "Tag"), + Value: jsPropToString(jsRecord, "Value"), + } + case dns.TypeHINFO: + rr = &dns.HINFO{ + Hdr: header, + Cpu: jsPropToString(jsRecord, "Cpu"), + Os: jsPropToString(jsRecord, "Os"), + } + case dns.TypeMINFO: + rr = &dns.MINFO{ + Hdr: header, + Email: jsPropToString(jsRecord, "Email"), + Rmail: jsPropToString(jsRecord, "Rmail"), + } + case dns.TypeISDN: + rr = &dns.ISDN{ + Hdr: header, + Address: jsPropToString(jsRecord, "Address"), + SubAddress: jsPropToString(jsRecord, "SubAddress"), + } + case dns.TypeKX: + rr = &dns.KX{ + Hdr: header, + Preference: jsPropToUint16(jsRecord, "Preference"), + Exchanger: jsPropToString(jsRecord, "Exchanger"), + } + case dns.TypeLOC: + rr = &dns.LOC{ + Hdr: header, + Version: jsPropToUint8(jsRecord, "Version"), + Size: jsPropToUint8(jsRecord, "Size"), + HorizPre: jsPropToUint8(jsRecord, "HorizPre"), + VertPre: jsPropToUint8(jsRecord, "VertPre"), + Latitude: jsPropToUint32(jsRecord, "Latitude"), + Longitude: jsPropToUint32(jsRecord, "Longitude"), + Altitude: jsPropToUint32(jsRecord, "Altitude"), + } + case dns.TypeSSHFP: + rr = &dns.SSHFP{ + Hdr: header, + Algorithm: jsPropToUint8(jsRecord, "Algorithm"), + FingerPrint: jsPropToString(jsRecord, "FingerPrint"), + Type: jsPropToUint8(jsRecord, "Type"), + } + case dns.TypeTLSA: + rr = &dns.TLSA{ + Hdr: header, + Certificate: jsPropToString(jsRecord, "Certificate"), + MatchingType: jsPropToUint8(jsRecord, "MatchingType"), + Selector: jsPropToUint8(jsRecord, "Selector"), + Usage: jsPropToUint8(jsRecord, "Usage"), + } + case dns.TypeCERT: + rr = &dns.CERT{ + Hdr: header, + Algorithm: jsPropToUint8(jsRecord, "Algorithm"), + Certificate: jsPropToString(jsRecord, "Certificate"), + KeyTag: jsPropToUint16(jsRecord, "KeyTag"), + Type: jsPropToUint16(jsRecord, "Type"), + } + case dns.TypeDS: + rr = &dns.DS{ + Hdr: header, + Algorithm: jsPropToUint8(jsRecord, "Algorithm"), + Digest: jsPropToString(jsRecord, "Digest"), + DigestType: jsPropToUint8(jsRecord, "DigestType"), + KeyTag: jsPropToUint16(jsRecord, "KeyTag"), + } + case dns.TypeNAPTR: + rr = &dns.NAPTR{ + Hdr: header, + Flags: jsPropToString(jsRecord, "Flags"), + Order: jsPropToUint16(jsRecord, "Order"), + Preference: jsPropToUint16(jsRecord, "Preference"), + Regexp: jsPropToString(jsRecord, "Regexp"), + Replacement: jsPropToString(jsRecord, "Replacement"), + Service: jsPropToString(jsRecord, "Service"), + } + case dns.TypeRRSIG: + rr = &dns.RRSIG{ + Hdr: header, + Algorithm: jsPropToUint8(jsRecord, "Algorithm"), + Expiration: jsPropToUint32(jsRecord, "Expiration"), + Inception: jsPropToUint32(jsRecord, "Inception"), + KeyTag: jsPropToUint16(jsRecord, "KeyTag"), + Labels: jsPropToUint8(jsRecord, "Labels"), + OrigTtl: jsPropToUint32(jsRecord, "OrigTtl"), + Signature: jsPropToString(jsRecord, "Signature"), + SignerName: jsPropToString(jsRecord, "SignerName"), + TypeCovered: jsPropToUint16(jsRecord, "TypeCovered"), + } + case dns.TypeNSEC: + rr = &dns.NSEC{ + Hdr: header, + NextDomain: jsPropToString(jsRecord, "NextDomain"), + TypeBitMap: jsPropToUint16Array(jsRecord, "TypeBitMap"), + } + case dns.TypeNSEC3: + rr = &dns.NSEC3{ + Hdr: header, + Flags: jsPropToUint8(jsRecord, "Flags"), + Hash: jsPropToUint8(jsRecord, "Hash"), + HashLength: jsPropToUint8(jsRecord, "HashLength"), + Iterations: jsPropToUint16(jsRecord, "Iterations"), + NextDomain: jsPropToString(jsRecord, "NextDomain"), + Salt: jsPropToString(jsRecord, "Salt"), + SaltLength: jsPropToUint8(jsRecord, "SaltLength"), + TypeBitMap: jsPropToUint16Array(jsRecord, "TypeBitMap"), + } + case dns.TypeNSEC3PARAM: + rr = &dns.NSEC3PARAM{ + Hdr: header, + Flags: jsPropToUint8(jsRecord, "Flags"), + Hash: jsPropToUint8(jsRecord, "Hash"), + Iterations: jsPropToUint16(jsRecord, "Iterations"), + Salt: jsPropToString(jsRecord, "Salt"), + SaltLength: jsPropToUint8(jsRecord, "SaltLength"), + } + case dns.TypeTKEY: + rr = &dns.TKEY{ + Hdr: header, + Algorithm: jsPropToString(jsRecord, "Algorithm"), + Error: jsPropToUint16(jsRecord, "Error"), + Expiration: jsPropToUint32(jsRecord, "Expiration"), + Inception: jsPropToUint32(jsRecord, "Inception"), + Key: jsPropToString(jsRecord, "Key"), + KeySize: jsPropToUint16(jsRecord, "KeySize"), + Mode: jsPropToUint16(jsRecord, "Mode"), + OtherData: jsPropToString(jsRecord, "OtherData"), + OtherLen: jsPropToUint16(jsRecord, "OtherLen"), + } + case dns.TypeTSIG: + rr = &dns.TSIG{ + Hdr: header, + Algorithm: jsPropToString(jsRecord, "Algorithm"), + Error: jsPropToUint16(jsRecord, "Error"), + Fudge: jsPropToUint16(jsRecord, "Fudge"), + MACSize: jsPropToUint16(jsRecord, "MACSize"), + MAC: jsPropToString(jsRecord, "MAC"), + OrigId: jsPropToUint16(jsRecord, "OrigId"), + OtherData: jsPropToString(jsRecord, "OtherData"), + OtherLen: jsPropToUint16(jsRecord, "OtherLen"), + TimeSigned: jsPropToUint64(jsRecord, "TimeSigned"), + } + case dns.TypeIPSECKEY: + rr = &dns.IPSECKEY{ + Hdr: header, + Algorithm: jsPropToUint8(jsRecord, "Algorithm"), + GatewayAddr: net.IP(jsPropToString(jsRecord, "GatewayAddr")), + GatewayHost: jsPropToString(jsRecord, "GatewayHost"), + GatewayType: jsPropToUint8(jsRecord, "GatewayType"), + Precedence: jsPropToUint8(jsRecord, "Precedence"), + PublicKey: jsPropToString(jsRecord, "PublicKey"), + } + case dns.TypeKEY: + rr = &dns.KEY{ + DNSKEY: dns.DNSKEY{ + Hdr: header, + Algorithm: jsPropToUint8(jsRecord, "Algorithm"), + Flags: jsPropToUint16(jsRecord, "Flags"), + Protocol: jsPropToUint8(jsRecord, "Protocol"), + PublicKey: jsPropToString(jsRecord, "PublicKey"), + }, + } + case dns.TypeCDS: + rr = &dns.CDS{ + DS: dns.DS{ + Hdr: header, + KeyTag: jsPropToUint16(jsRecord, "KeyTag"), + Algorithm: jsPropToUint8(jsRecord, "Algorithm"), + DigestType: jsPropToUint8(jsRecord, "DigestType"), + Digest: jsPropToString(jsRecord, "Digest"), + }, + } + case dns.TypeCDNSKEY: + rr = &dns.CDNSKEY{ + DNSKEY: dns.DNSKEY{ + Hdr: header, + Algorithm: jsPropToUint8(jsRecord, "Algorithm"), + Flags: jsPropToUint16(jsRecord, "Flags"), + Protocol: jsPropToUint8(jsRecord, "Protocol"), + PublicKey: jsPropToString(jsRecord, "PublicKey"), + }, + } + case dns.TypeNID: + rr = &dns.NID{ + Hdr: header, + NodeID: jsPropToUint64(jsRecord, "NodeID"), + Preference: jsPropToUint16(jsRecord, "Preference"), + } + case dns.TypeL32: + rr = &dns.L32{ + Hdr: header, + Locator32: net.IP(jsPropToString(jsRecord, "Locator32")), + Preference: jsPropToUint16(jsRecord, "Preference"), + } + case dns.TypeL64: + rr = &dns.L64{ + Hdr: header, + Locator64: jsPropToUint64(jsRecord, "Locator64"), + Preference: jsPropToUint16(jsRecord, "Preference"), + } + case dns.TypeLP: + rr = &dns.LP{ + Hdr: header, + Fqdn: jsPropToString(jsRecord, "Fqdn"), + Preference: jsPropToUint16(jsRecord, "Preference"), + } + case dns.TypeGPOS: + rr = &dns.GPOS{ + Hdr: header, + Altitude: jsPropToString(jsRecord, "Altitude"), + Latitude: jsPropToString(jsRecord, "Latitude"), + Longitude: jsPropToString(jsRecord, "Longitude"), + } + case dns.TypeRP: + rr = &dns.RP{ + Hdr: header, + Mbox: jsPropToString(jsRecord, "Mbox"), + Txt: jsPropToString(jsRecord, "Txt"), + } + case dns.TypeRKEY: + rr = &dns.RKEY{ + Hdr: header, + Algorithm: jsPropToUint8(jsRecord, "Algorithm"), + Flags: jsPropToUint16(jsRecord, "Flags"), + Protocol: jsPropToUint8(jsRecord, "Protocol"), + PublicKey: jsPropToString(jsRecord, "PublicKey"), + } + case dns.TypeSMIMEA: + rr = &dns.SMIMEA{ + Hdr: header, + Certificate: jsPropToString(jsRecord, "Certificate"), + MatchingType: jsPropToUint8(jsRecord, "MatchingType"), + Selector: jsPropToUint8(jsRecord, "Selector"), + Usage: jsPropToUint8(jsRecord, "Usage"), + } + case dns.TypeAMTRELAY: + rr = &dns.AMTRELAY{ + Hdr: header, + GatewayAddr: net.IP(jsPropToString(jsRecord, "GatewayAddr")), + GatewayHost: jsPropToString(jsRecord, "GatewayHost"), + GatewayType: jsPropToUint8(jsRecord, "GatewayType"), + Precedence: jsPropToUint8(jsRecord, "Precedence"), + } + case dns.TypeAVC: + rr = &dns.AVC{ + Hdr: header, + Txt: jsPropToStringArray(jsRecord, "Txt"), + } + case dns.TypeURI: + rr = &dns.URI{ + Hdr: header, + Priority: jsPropToUint16(jsRecord, "Priority"), + Weight: jsPropToUint16(jsRecord, "Weight"), + Target: jsPropToString(jsRecord, "Target"), + } + case dns.TypeEUI48: + rr = &dns.EUI48{ + Hdr: header, + Address: jsPropToUint64(jsRecord, "Address"), + } + case dns.TypeEUI64: + rr = &dns.EUI64{ + Hdr: header, + Address: jsPropToUint64(jsRecord, "Address"), + } + case dns.TypeGID: + rr = &dns.GID{ + Hdr: header, + Gid: jsPropToUint32(jsRecord, "Gid"), + } + case dns.TypeUID: + rr = &dns.UID{ + Hdr: header, + Uid: jsPropToUint32(jsRecord, "Uid"), + } + case dns.TypeUINFO: + rr = &dns.UINFO{ + Hdr: header, + Uinfo: jsPropToString(jsRecord, "Uinfo"), + } + case dns.TypeSPF: + rr = &dns.SPF{ + Hdr: header, + Txt: jsPropToStringArray(jsRecord, "Txt"), + } + case dns.TypeHTTPS: + jsKvs := jsPropToMapArray(jsRecord, "Value") + var kvs []dns.SVCBKeyValue + for _, jsKv := range jsKvs { + kv, err := ToSVCBKeyValue(jsKv) + if err != nil { + log.Error(err.Error()) + continue + } + kvs = append(kvs, kv) + } + rr = &dns.HTTPS{ + SVCB: dns.SVCB{ + Hdr: header, + Priority: jsPropToUint16(jsRecord, "Priority"), + Target: jsPropToString(jsRecord, "Target"), + Value: kvs, + }, + } + case dns.TypeSVCB: + jsKvs := jsPropToMapArray(jsRecord, "Value") + var kvs []dns.SVCBKeyValue + for _, jsKv := range jsKvs { + kv, err := ToSVCBKeyValue(jsKv) + if err != nil { + log.Error(err.Error()) + continue + } + kvs = append(kvs, kv) + } + rr = &dns.SVCB{ + Hdr: header, + Priority: jsPropToUint16(jsRecord, "Priority"), + Target: jsPropToString(jsRecord, "Target"), + Value: kvs, + } + case dns.TypeZONEMD: + rr = &dns.ZONEMD{ + Hdr: header, + Digest: jsPropToString(jsRecord, "Digest"), + Hash: jsPropToUint8(jsRecord, "Hash"), + Scheme: jsPropToUint8(jsRecord, "Scheme"), + Serial: jsPropToUint32(jsRecord, "Serial"), + } + case dns.TypeCSYNC: + rr = &dns.CSYNC{ + Hdr: header, + Flags: jsPropToUint16(jsRecord, "Flags"), + Serial: jsPropToUint32(jsRecord, "Serial"), + TypeBitMap: jsPropToUint16Array(jsRecord, "TypeBitMap"), + } + case dns.TypeOPENPGPKEY: + rr = &dns.OPENPGPKEY{ + Hdr: header, + PublicKey: jsPropToString(jsRecord, "PublicKey"), + } + case dns.TypeTALINK: + rr = &dns.TALINK{ + Hdr: header, + NextName: jsPropToString(jsRecord, "NextName"), + PreviousName: jsPropToString(jsRecord, "PreviousName"), + } + case dns.TypeNINFO: + rr = &dns.NINFO{ + Hdr: header, + ZSData: jsPropToStringArray(jsRecord, "ZSData"), + } + case dns.TypeDHCID: + rr = &dns.DHCID{ + Hdr: header, + Digest: jsPropToString(jsRecord, "Digest"), + } + case dns.TypeDNSKEY: + rr = &dns.DNSKEY{ + Hdr: header, + Algorithm: jsPropToUint8(jsRecord, "Algorithm"), + Flags: jsPropToUint16(jsRecord, "Flags"), + Protocol: jsPropToUint8(jsRecord, "Protocol"), + PublicKey: jsPropToString(jsRecord, "PublicKey"), + } + case dns.TypeHIP: + rr = &dns.HIP{ + Hdr: header, + Hit: jsPropToString(jsRecord, "Hit"), + HitLength: jsPropToUint8(jsRecord, "HitLength"), + PublicKey: jsPropToString(jsRecord, "PublicKey"), + PublicKeyAlgorithm: jsPropToUint8(jsRecord, "PublicKeyAlgorithm"), + PublicKeyLength: jsPropToUint16(jsRecord, "PublicKeyLength"), + RendezvousServers: jsPropToStringArray(jsRecord, "RendezvousServers"), + } + case dns.TypeOPT: + jsOptions := jsPropToMapArray(jsRecord, "Option") + var options []dns.EDNS0 + for _, jsOption := range jsOptions { + option, err := ToEDNS0(jsOption) + if err != nil { + log.Error(err.Error()) + continue + } + options = append(options, option) + } + rr = &dns.OPT{ + Hdr: header, + Option: options, + } + case dns.TypeNIMLOC: + rr = &dns.NIMLOC{ + Hdr: header, + Locator: jsPropToString(jsRecord, "Locator"), + } + case dns.TypeEID: + rr = &dns.EID{ + Hdr: header, + Endpoint: jsPropToString(jsRecord, "Endpoint"), + } + case dns.TypeNXT: + rr = &dns.NXT{ + NSEC: dns.NSEC{ + Hdr: header, + NextDomain: jsPropToString(jsRecord, "NextDomain"), + TypeBitMap: jsPropToUint16Array(jsRecord, "TypeBitMap"), + }, + } + case dns.TypePX: + rr = &dns.PX{ + Hdr: header, + Mapx400: jsPropToString(jsRecord, "Mapx400"), + Map822: jsPropToString(jsRecord, "Map822"), + Preference: jsPropToUint16(jsRecord, "Preference"), + } + case dns.TypeSIG: + rr = &dns.SIG{ + RRSIG: dns.RRSIG{ + Hdr: header, + Algorithm: jsPropToUint8(jsRecord, "Algorithm"), + Expiration: jsPropToUint32(jsRecord, "Expiration"), + Inception: jsPropToUint32(jsRecord, "Inception"), + KeyTag: jsPropToUint16(jsRecord, "KeyTag"), + Labels: jsPropToUint8(jsRecord, "Labels"), + OrigTtl: jsPropToUint32(jsRecord, "OrigTtl"), + Signature: jsPropToString(jsRecord, "Signature"), + SignerName: jsPropToString(jsRecord, "SignerName"), + TypeCovered: jsPropToUint16(jsRecord, "TypeCovered"), + }, + } + case dns.TypeRT: + rr = &dns.RT{ + Hdr: header, + Host: jsPropToString(jsRecord, "Host"), + Preference: jsPropToUint16(jsRecord, "Preference"), + } + case dns.TypeNSAPPTR: + rr = &dns.NSAPPTR{ + Hdr: header, + Ptr: jsPropToString(jsRecord, "Ptr"), + } + case dns.TypeX25: + rr = &dns.X25{ + Hdr: header, + PSDNAddress: jsPropToString(jsRecord, "PSDNAddress"), + } + // case dns.TypeATMA: + // case dns.TypeWKS: + // case dns.TypeDOA: + // case dns.TypeSINK: + default: + return nil, fmt.Errorf("error converting to dns.RR: unknown type: %d", header.Rrtype) + } + + return rr, nil +} diff --git a/modules/dns_proxy/dns_proxy_js_record_edns0.go b/modules/dns_proxy/dns_proxy_js_record_edns0.go new file mode 100644 index 00000000..297c9957 --- /dev/null +++ b/modules/dns_proxy/dns_proxy_js_record_edns0.go @@ -0,0 +1,208 @@ +package dns_proxy + +import ( + "fmt" + "net" + + "github.com/bettercap/bettercap/v2/log" + "github.com/miekg/dns" +) + +func NewJSEDNS0(e dns.EDNS0) (jsEDNS0 map[string]interface{}, err error) { + option := e.Option() + + jsEDNS0 = map[string]interface{}{ + "Option": option, + } + + var jsVal map[string]interface{} + + switch opt := e.(type) { + case *dns.EDNS0_LLQ: + jsVal = map[string]interface{}{ + "Code": opt.Code, + "Error": opt.Error, + "Id": opt.Id, + "LeaseLife": opt.LeaseLife, + "Opcode": opt.Opcode, + "Version": opt.Version, + } + case *dns.EDNS0_UL: + jsVal = map[string]interface{}{ + "Code": opt.Code, + "Lease": opt.Lease, + "KeyLease": opt.KeyLease, + } + case *dns.EDNS0_NSID: + jsVal = map[string]interface{}{ + "Code": opt.Code, + "Nsid": opt.Nsid, + } + case *dns.EDNS0_ESU: + jsVal = map[string]interface{}{ + "Code": opt.Code, + "Uri": opt.Uri, + } + case *dns.EDNS0_DAU: + jsVal = map[string]interface{}{ + "AlgCode": opt.AlgCode, + "Code": opt.Code, + } + case *dns.EDNS0_DHU: + jsVal = map[string]interface{}{ + "AlgCode": opt.AlgCode, + "Code": opt.Code, + } + case *dns.EDNS0_N3U: + jsVal = map[string]interface{}{ + "AlgCode": opt.AlgCode, + "Code": opt.Code, + } + case *dns.EDNS0_SUBNET: + jsVal = map[string]interface{}{ + "Address": opt.Address.String(), + "Code": opt.Code, + "Family": opt.Family, + "SourceNetmask": opt.SourceNetmask, + "SourceScope": opt.SourceScope, + } + case *dns.EDNS0_EXPIRE: + jsVal = map[string]interface{}{ + "Code": opt.Code, + "Empty": opt.Empty, + "Expire": opt.Expire, + } + case *dns.EDNS0_COOKIE: + jsVal = map[string]interface{}{ + "Code": opt.Code, + "Cookie": opt.Cookie, + } + case *dns.EDNS0_TCP_KEEPALIVE: + jsVal = map[string]interface{}{ + "Code": opt.Code, + "Length": opt.Length, + "Timeout": opt.Timeout, + } + case *dns.EDNS0_PADDING: + jsVal = map[string]interface{}{ + "Padding": string(opt.Padding), + } + case *dns.EDNS0_EDE: + jsVal = map[string]interface{}{ + "ExtraText": opt.ExtraText, + "InfoCode": opt.InfoCode, + } + case *dns.EDNS0_LOCAL: + jsVal = map[string]interface{}{ + "Code": opt.Code, + "Data": string(opt.Data), + } + default: + return nil, fmt.Errorf("unsupported EDNS0 option: %d", option) + } + + jsEDNS0["Value"] = jsVal + + return jsEDNS0, nil +} + +func ToEDNS0(jsEDNS0 map[string]interface{}) (e dns.EDNS0, err error) { + option := jsPropToUint16(jsEDNS0, "Option") + + jsVal := jsPropToMap(jsEDNS0, "Value") + + switch option { + case dns.EDNS0LLQ: + e = &dns.EDNS0_LLQ{ + Code: jsPropToUint16(jsVal, "Code"), + Error: jsPropToUint16(jsVal, "Error"), + Id: jsPropToUint64(jsVal, "Id"), + LeaseLife: jsPropToUint32(jsVal, "LeaseLife"), + Opcode: jsPropToUint16(jsVal, "Opcode"), + Version: jsPropToUint16(jsVal, "Version"), + } + case dns.EDNS0UL: + e = &dns.EDNS0_UL{ + Code: jsPropToUint16(jsVal, "Code"), + Lease: jsPropToUint32(jsVal, "Lease"), + KeyLease: jsPropToUint32(jsVal, "KeyLease"), + } + case dns.EDNS0NSID: + e = &dns.EDNS0_NSID{ + Code: jsPropToUint16(jsVal, "Code"), + Nsid: jsPropToString(jsVal, "Nsid"), + } + case dns.EDNS0ESU: + e = &dns.EDNS0_ESU{ + Code: jsPropToUint16(jsVal, "Code"), + Uri: jsPropToString(jsVal, "Uri"), + } + case dns.EDNS0DAU: + e = &dns.EDNS0_DAU{ + AlgCode: jsPropToUint8Array(jsVal, "AlgCode"), + Code: jsPropToUint16(jsVal, "Code"), + } + case dns.EDNS0DHU: + e = &dns.EDNS0_DHU{ + AlgCode: jsPropToUint8Array(jsVal, "AlgCode"), + Code: jsPropToUint16(jsVal, "Code"), + } + case dns.EDNS0N3U: + e = &dns.EDNS0_N3U{ + AlgCode: jsPropToUint8Array(jsVal, "AlgCode"), + Code: jsPropToUint16(jsVal, "Code"), + } + case dns.EDNS0SUBNET: + e = &dns.EDNS0_SUBNET{ + Address: net.ParseIP(jsPropToString(jsVal, "Address")), + Code: jsPropToUint16(jsVal, "Code"), + Family: jsPropToUint16(jsVal, "Family"), + SourceNetmask: jsPropToUint8(jsVal, "SourceNetmask"), + SourceScope: jsPropToUint8(jsVal, "SourceScope"), + } + case dns.EDNS0EXPIRE: + if empty, ok := jsVal["Empty"].(bool); !ok { + log.Error("invalid or missing EDNS0_EXPIRE.Empty bool value, skipping field.") + e = &dns.EDNS0_EXPIRE{ + Code: jsPropToUint16(jsVal, "Code"), + Expire: jsPropToUint32(jsVal, "Expire"), + } + } else { + e = &dns.EDNS0_EXPIRE{ + Code: jsPropToUint16(jsVal, "Code"), + Expire: jsPropToUint32(jsVal, "Expire"), + Empty: empty, + } + } + case dns.EDNS0COOKIE: + e = &dns.EDNS0_COOKIE{ + Code: jsPropToUint16(jsVal, "Code"), + Cookie: jsPropToString(jsVal, "Cookie"), + } + case dns.EDNS0TCPKEEPALIVE: + e = &dns.EDNS0_TCP_KEEPALIVE{ + Code: jsPropToUint16(jsVal, "Code"), + Length: jsPropToUint16(jsVal, "Length"), + Timeout: jsPropToUint16(jsVal, "Timeout"), + } + case dns.EDNS0PADDING: + e = &dns.EDNS0_PADDING{ + Padding: []byte(jsPropToString(jsVal, "Padding")), + } + case dns.EDNS0EDE: + e = &dns.EDNS0_EDE{ + ExtraText: jsPropToString(jsVal, "ExtraText"), + InfoCode: jsPropToUint16(jsVal, "InfoCode"), + } + case dns.EDNS0LOCALSTART, dns.EDNS0LOCALEND, 0x8000: + // _DO = 0x8000 + e = &dns.EDNS0_LOCAL{ + Code: jsPropToUint16(jsVal, "Code"), + Data: []byte(jsPropToString(jsVal, "Data")), + } + default: + return nil, fmt.Errorf("unsupported EDNS0 option: %d", option) + } + + return e, nil +} diff --git a/modules/dns_proxy/dns_proxy_js_record_svcb.go b/modules/dns_proxy/dns_proxy_js_record_svcb.go new file mode 100644 index 00000000..f0f0242d --- /dev/null +++ b/modules/dns_proxy/dns_proxy_js_record_svcb.go @@ -0,0 +1,127 @@ +package dns_proxy + +import ( + "fmt" + "net" + + "github.com/bettercap/bettercap/v2/log" + "github.com/miekg/dns" +) + +func NewJSSVCBKeyValue(kv dns.SVCBKeyValue) (map[string]interface{}, error) { + key := kv.Key() + + jsKv := map[string]interface{}{ + "Key": uint16(key), + } + + switch v := kv.(type) { + case *dns.SVCBAlpn: + jsKv["Alpn"] = v.Alpn + case *dns.SVCBNoDefaultAlpn: + break + case *dns.SVCBECHConfig: + jsKv["ECH"] = string(v.ECH) + case *dns.SVCBPort: + jsKv["Port"] = v.Port + case *dns.SVCBIPv4Hint: + ips := v.Hint + jsIps := make([]string, len(ips)) + for i, ip := range ips { + jsIps[i] = ip.String() + } + jsKv["Hint"] = jsIps + case *dns.SVCBIPv6Hint: + ips := v.Hint + jsIps := make([]string, len(ips)) + for i, ip := range ips { + jsIps[i] = ip.String() + } + jsKv["Hint"] = jsIps + case *dns.SVCBDoHPath: + jsKv["Template"] = v.Template + case *dns.SVCBOhttp: + break + case *dns.SVCBMandatory: + keys := v.Code + jsKeys := make([]uint16, len(keys)) + for i, _key := range keys { + jsKeys[i] = uint16(_key) + } + jsKv["Code"] = jsKeys + default: + return nil, fmt.Errorf("error creating JSSVCBKeyValue: unknown key: %d", key) + } + + return jsKv, nil +} + +func ToSVCBKeyValue(jsKv map[string]interface{}) (dns.SVCBKeyValue, error) { + var kv dns.SVCBKeyValue + + key := dns.SVCBKey(jsPropToUint16(jsKv, "Key")) + + switch key { + case dns.SVCB_ALPN: + kv = &dns.SVCBAlpn{ + Alpn: jsPropToStringArray(jsKv, "Value"), + } + case dns.SVCB_NO_DEFAULT_ALPN: + kv = &dns.SVCBNoDefaultAlpn{} + case dns.SVCB_ECHCONFIG: + kv = &dns.SVCBECHConfig{ + ECH: []byte(jsPropToString(jsKv, "Value")), + } + case dns.SVCB_PORT: + kv = &dns.SVCBPort{ + Port: jsPropToUint16(jsKv, "Value"), + } + case dns.SVCB_IPV4HINT: + jsIps := jsPropToStringArray(jsKv, "Value") + var ips []net.IP + for _, jsIp := range jsIps { + ip := net.ParseIP(jsIp) + if ip == nil { + log.Error("error converting to SVCBKeyValue: invalid IPv4Hint IP: %s", jsIp) + continue + } + ips = append(ips, ip) + } + kv = &dns.SVCBIPv4Hint{ + Hint: ips, + } + case dns.SVCB_IPV6HINT: + jsIps := jsPropToStringArray(jsKv, "Value") + var ips []net.IP + for _, jsIp := range jsIps { + ip := net.ParseIP(jsIp) + if ip == nil { + log.Error("error converting to SVCBKeyValue: invalid IPv6Hint IP: %s", jsIp) + continue + } + ips = append(ips, ip) + } + kv = &dns.SVCBIPv6Hint{ + Hint: ips, + } + case dns.SVCB_DOHPATH: + kv = &dns.SVCBDoHPath{ + Template: jsPropToString(jsKv, "Value"), + } + case dns.SVCB_OHTTP: + kv = &dns.SVCBOhttp{} + case dns.SVCB_MANDATORY: + v := jsPropToUint16Array(jsKv, "Value") + keys := make([]dns.SVCBKey, len(v)) + for i, jsKey := range v { + keys[i] = dns.SVCBKey(jsKey) + } + kv = &dns.SVCBMandatory{ + Code: keys, + } + default: + return nil, fmt.Errorf("error converting to dns.SVCBKeyValue: unknown key: %d", key) + } + + return kv, nil +} diff --git a/tls/cert.go b/tls/cert.go index 099c9b7a..aa804611 100644 --- a/tls/cert.go +++ b/tls/cert.go @@ -40,6 +40,14 @@ var ( OrganizationalUnit: "https://certs.godaddy.com/repository/", CommonName: "Go Daddy Secure Certificate Authority - G2", } + DefaultCloudflareDNSConfig = CertConfig{ + Bits: 4096, + Country: "US", + Locality: "San Francisco", + Organization: "Cloudflare, Inc.", + OrganizationalUnit: "", + CommonName: "cloudflare-dns.com", + } ) func CertConfigToModule(prefix string, m *session.SessionModule, defaults CertConfig) { From c5d93825bd37ad11936c26970cb95822ad8d5cfe Mon Sep 17 00:00:00 2001 From: buffermet <29265684+buffermet@users.noreply.github.com> Date: Wed, 9 Oct 2024 20:07:22 +0200 Subject: [PATCH 03/10] Catch RR nil value, improve debug logs readability. --- modules/dns_proxy/dns_proxy_base_filters.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/dns_proxy/dns_proxy_base_filters.go b/modules/dns_proxy/dns_proxy_base_filters.go index ad12db76..d66b0ad1 100644 --- a/modules/dns_proxy/dns_proxy_base_filters.go +++ b/modules/dns_proxy/dns_proxy_base_filters.go @@ -17,7 +17,9 @@ func questionsToStrings(qs []dns.Question) []string { func recordsToStrings(rrs []dns.RR) []string { records := []string{} for _, rr := range rrs { - records = append(records, tabsToSpaces(rr.String())) + if rr != nil { + records = append(records, tabsToSpaces(rr.String())) + } } return records } @@ -57,7 +59,7 @@ func (p *DNSProxy) logResponseAction(m *dns.Msg, clientIP string) { } func (p *DNSProxy) onRequestFilter(query *dns.Msg, clientIP string) (req, res *dns.Msg) { - p.Debug("< %s %s", + p.Debug("< %s q[%s]", clientIP, strings.Join(questionsToStrings(query.Question), ",")) @@ -89,7 +91,7 @@ func (p *DNSProxy) onResponseFilter(req, res *dns.Msg, clientIP string) *dns.Msg return nil } - p.Debug("> %s %s [%s] [%s] [%s]", + p.Debug("> %s q[%s] a[%s] e[%s] n[%s]", clientIP, strings.Join(questionsToStrings(res.Question), ","), strings.Join(recordsToStrings(res.Answer), ","), From c5017ed02001fd75638b81c18f320ab366870197 Mon Sep 17 00:00:00 2001 From: buffermet <29265684+buffermet@users.noreply.github.com> Date: Sat, 12 Oct 2024 13:45:27 +0200 Subject: [PATCH 04/10] Support RFC3597 generic/unknown record type if Rdata field is present. --- modules/dns_proxy/dns_proxy_js_query.go | 70 +++++++++++++++++++++ modules/dns_proxy/dns_proxy_js_record.go | 79 +++--------------------- 2 files changed, 78 insertions(+), 71 deletions(-) diff --git a/modules/dns_proxy/dns_proxy_js_query.go b/modules/dns_proxy/dns_proxy_js_query.go index 7db5118a..5e563561 100644 --- a/modules/dns_proxy/dns_proxy_js_query.go +++ b/modules/dns_proxy/dns_proxy_js_query.go @@ -36,6 +36,76 @@ type JSQueryHeader struct { Zero bool } +func jsPropToMap(obj map[string]interface{}, key string) map[string]interface{} { + if v, ok := obj[key].(map[string]interface{}); ok { + return v + } + return map[string]interface{}{} +} + +func jsPropToMapArray(obj map[string]interface{}, key string) []map[string]interface{} { + if v, ok := obj[key].([]map[string]interface{}); ok { + return v + } + return []map[string]interface{}{} +} + +func jsPropToString(obj map[string]interface{}, key string) string { + if v, ok := obj[key].(string); ok { + return v + } + return "" +} + +func jsPropToStringArray(obj map[string]interface{}, key string) []string { + if v, ok := obj[key].([]string); ok { + return v + } + return []string{} +} + +func jsPropToUint8(obj map[string]interface{}, key string) uint8 { + if v, ok := obj[key].(uint8); ok { + return v + } + return 0 +} + +func jsPropToUint8Array(obj map[string]interface{}, key string) []uint8 { + if v, ok := obj[key].([]uint8); ok { + return v + } + return []uint8{} +} + +func jsPropToUint16(obj map[string]interface{}, key string) uint16 { + if v, ok := obj[key].(uint16); ok { + return v + } + return 0 +} + +func jsPropToUint16Array(obj map[string]interface{}, key string) []uint16 { + if v, ok := obj[key].([]uint16); ok { + return v + } + return []uint16{} +} + +func jsPropToUint32(obj map[string]interface{}, key string) uint32 { + if v, ok := obj[key].(uint32); ok { + return v + } + return 0 +} + +func jsPropToUint64(obj map[string]interface{}, key string) uint64 { + if v, ok := obj[key].(uint64); ok { + return v + } + return 0 +} + func (j *JSQuery) NewHash() string { answers, _ := json.Marshal(j.Answers) extras, _ := json.Marshal(j.Extras) diff --git a/modules/dns_proxy/dns_proxy_js_record.go b/modules/dns_proxy/dns_proxy_js_record.go index 647267e2..55832d69 100644 --- a/modules/dns_proxy/dns_proxy_js_record.go +++ b/modules/dns_proxy/dns_proxy_js_record.go @@ -8,76 +8,6 @@ import ( "github.com/miekg/dns" ) -func jsPropToMap(obj map[string]interface{}, key string) map[string]interface{} { - if v, ok := obj[key].(map[string]interface{}); ok { - return v - } - return map[string]interface{}{} -} - -func jsPropToMapArray(obj map[string]interface{}, key string) []map[string]interface{} { - if v, ok := obj[key].([]map[string]interface{}); ok { - return v - } - return []map[string]interface{}{} -} - -func jsPropToString(obj map[string]interface{}, key string) string { - if v, ok := obj[key].(string); ok { - return v - } - return "" -} - -func jsPropToStringArray(obj map[string]interface{}, key string) []string { - if v, ok := obj[key].([]string); ok { - return v - } - return []string{} -} - -func jsPropToUint8(obj map[string]interface{}, key string) uint8 { - if v, ok := obj[key].(uint8); ok { - return v - } - return 0 -} - -func jsPropToUint8Array(obj map[string]interface{}, key string) []uint8 { - if v, ok := obj[key].([]uint8); ok { - return v - } - return []uint8{} -} - -func jsPropToUint16(obj map[string]interface{}, key string) uint16 { - if v, ok := obj[key].(uint16); ok { - return v - } - return 0 -} - -func jsPropToUint16Array(obj map[string]interface{}, key string) []uint16 { - if v, ok := obj[key].([]uint16); ok { - return v - } - return []uint16{} -} - -func jsPropToUint32(obj map[string]interface{}, key string) uint32 { - if v, ok := obj[key].(uint32); ok { - return v - } - return 0 -} - -func jsPropToUint64(obj map[string]interface{}, key string) uint64 { - if v, ok := obj[key].(uint64); ok { - return v - } - return 0 -} - func NewJSResourceRecord(rr dns.RR) (jsRecord map[string]interface{}, err error) { header := rr.Header() @@ -1007,7 +937,14 @@ func ToRR(jsRecord map[string]interface{}) (rr dns.RR, err error) { // case dns.TypeDOA: // case dns.TypeSINK: default: - return nil, fmt.Errorf("error converting to dns.RR: unknown type: %d", header.Rrtype) + if rdata, ok := jsRecord["Rdata"].(string); ok { + rr = &dns.RFC3597{ + Hdr: header, + Rdata: rdata, + } + } else { + return nil, fmt.Errorf("error converting to dns.RR: unknown type: %d", header.Rrtype) + } } return rr, nil From fe9481cb429ee88a0f52c7ecd27d28a0dc3c2c01 Mon Sep 17 00:00:00 2001 From: buffermet <29265684+buffermet@users.noreply.github.com> Date: Sat, 12 Oct 2024 17:34:14 +0200 Subject: [PATCH 05/10] Print JS property conversion errors. --- modules/dns_proxy/dns_proxy_js_query.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/modules/dns_proxy/dns_proxy_js_query.go b/modules/dns_proxy/dns_proxy_js_query.go index 5e563561..cd38f01f 100644 --- a/modules/dns_proxy/dns_proxy_js_query.go +++ b/modules/dns_proxy/dns_proxy_js_query.go @@ -40,6 +40,7 @@ func jsPropToMap(obj map[string]interface{}, key string) map[string]interface{} if v, ok := obj[key].(map[string]interface{}); ok { return v } + log.Debug("error converting JS property to map[string]interface{} where key is: %s", key) return map[string]interface{}{} } @@ -47,6 +48,7 @@ func jsPropToMapArray(obj map[string]interface{}, key string) []map[string]inter if v, ok := obj[key].([]map[string]interface{}); ok { return v } + log.Debug("error converting JS property to []map[string]interface{} where key is: %s", key) return []map[string]interface{}{} } @@ -54,6 +56,7 @@ func jsPropToString(obj map[string]interface{}, key string) string { if v, ok := obj[key].(string); ok { return v } + log.Debug("error converting JS property to string where key is: %s", key) return "" } @@ -61,6 +64,7 @@ func jsPropToStringArray(obj map[string]interface{}, key string) []string { if v, ok := obj[key].([]string); ok { return v } + log.Debug("error converting JS property to []string where key is: %s", key) return []string{} } @@ -68,6 +72,7 @@ func jsPropToUint8(obj map[string]interface{}, key string) uint8 { if v, ok := obj[key].(uint8); ok { return v } + log.Debug("error converting JS property to uint8 where key is: %s", key) return 0 } @@ -75,6 +80,7 @@ func jsPropToUint8Array(obj map[string]interface{}, key string) []uint8 { if v, ok := obj[key].([]uint8); ok { return v } + log.Debug("error converting JS property to []uint8 where key is: %s", key) return []uint8{} } @@ -82,6 +88,7 @@ func jsPropToUint16(obj map[string]interface{}, key string) uint16 { if v, ok := obj[key].(uint16); ok { return v } + log.Debug("error converting JS property to uint16 where key is: %s", key) return 0 } @@ -89,6 +96,7 @@ func jsPropToUint16Array(obj map[string]interface{}, key string) []uint16 { if v, ok := obj[key].([]uint16); ok { return v } + log.Debug("error converting JS property to []uint16 where key is: %s", key) return []uint16{} } @@ -96,6 +104,7 @@ func jsPropToUint32(obj map[string]interface{}, key string) uint32 { if v, ok := obj[key].(uint32); ok { return v } + log.Debug("error converting JS property to uint32 where key is: %s", key) return 0 } @@ -103,6 +112,7 @@ func jsPropToUint64(obj map[string]interface{}, key string) uint64 { if v, ok := obj[key].(uint64); ok { return v } + log.Debug("error converting JS property to uint64 where key is: %s", key) return 0 } From 27d245625c281885bdbd0138f87023efe8fe7f3f Mon Sep 17 00:00:00 2001 From: buffermet <29265684+buffermet@users.noreply.github.com> Date: Sat, 12 Oct 2024 17:38:49 +0200 Subject: [PATCH 06/10] Remove redundant nil assignment. --- modules/dns_proxy/dns_proxy_base.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/dns_proxy/dns_proxy_base.go b/modules/dns_proxy/dns_proxy_base.go index b9652237..8eb4e0f6 100644 --- a/modules/dns_proxy/dns_proxy_base.go +++ b/modules/dns_proxy/dns_proxy_base.go @@ -55,8 +55,6 @@ func (p *DNSProxy) Configure(address string, dnsPort int, doRedirect bool, names } else { p.Debug("proxy script %s loaded.", scriptPath) } - } else { - p.Script = nil } dnsClient := dns.Client{ From 40f3906115034f1c947c058b398b60b98b39511c Mon Sep 17 00:00:00 2001 From: buffermet <29265684+buffermet@users.noreply.github.com> Date: Sat, 12 Oct 2024 21:47:02 +0200 Subject: [PATCH 07/10] Add blacklist and whitelist logic. --- modules/dns_proxy/dns_proxy.go | 4 +- modules/dns_proxy/dns_proxy_base.go | 18 +++++ modules/dns_proxy/dns_proxy_base_filters.go | 78 +++++++++++---------- 3 files changed, 61 insertions(+), 39 deletions(-) diff --git a/modules/dns_proxy/dns_proxy.go b/modules/dns_proxy/dns_proxy.go index f27cde2c..76de6db9 100644 --- a/modules/dns_proxy/dns_proxy.go +++ b/modules/dns_proxy/dns_proxy.go @@ -113,10 +113,10 @@ func NewDnsProxy(s *session.Session) *DnsProxy { "Address to bind the DNS proxy to.")) mod.AddParam(session.NewStringParameter("dns.proxy.blacklist", "", "", - "Comma separated list of hostnames to skip while proxying (wildcard expressions can be used).")) + "Comma separated list of client IPs to skip while proxying.")) mod.AddParam(session.NewStringParameter("dns.proxy.whitelist", "", "", - "Comma separated list of hostnames to proxy if the blacklist is used (wildcard expressions can be used).")) + "Comma separated list of client IPs to proxy if the blacklist is used.")) mod.AddParam(session.NewStringParameter("dns.proxy.nameserver", "1.1.1.1", diff --git a/modules/dns_proxy/dns_proxy_base.go b/modules/dns_proxy/dns_proxy_base.go index 8eb4e0f6..ac637bf3 100644 --- a/modules/dns_proxy/dns_proxy_base.go +++ b/modules/dns_proxy/dns_proxy_base.go @@ -41,6 +41,24 @@ type DNSProxy struct { tag string } +func (p *DNSProxy) shouldProxy(clientIP string) bool { + // check if this client is in the whitelist + for _, ip := range p.Whitelist { + if clientIP == ip { + return true + } + } + + // check if this client is in the blacklist + for _, ip := range p.Blacklist { + if clientIP == ip { + return false + } + } + + return true +} + func (p *DNSProxy) Configure(address string, dnsPort int, doRedirect bool, nameserver string, netProtocol string, proxyPort int, scriptPath string, certFile string, keyFile string) error { var err error diff --git a/modules/dns_proxy/dns_proxy_base_filters.go b/modules/dns_proxy/dns_proxy_base_filters.go index d66b0ad1..60cdd5dd 100644 --- a/modules/dns_proxy/dns_proxy_base_filters.go +++ b/modules/dns_proxy/dns_proxy_base_filters.go @@ -59,53 +59,57 @@ func (p *DNSProxy) logResponseAction(m *dns.Msg, clientIP string) { } func (p *DNSProxy) onRequestFilter(query *dns.Msg, clientIP string) (req, res *dns.Msg) { - p.Debug("< %s q[%s]", - clientIP, - strings.Join(questionsToStrings(query.Question), ",")) + if p.shouldProxy(clientIP) { + p.Debug("< %s q[%s]", + clientIP, + strings.Join(questionsToStrings(query.Question), ",")) - // do we have a proxy script? - if p.Script == nil { - return query, nil - } + // do we have a proxy script? + if p.Script == nil { + return query, nil + } - // run the module OnRequest callback if defined - jsreq, jsres := p.Script.OnRequest(query, clientIP) - if jsreq != nil { - // the request has been changed by the script - req := jsreq.ToQuery() - p.logRequestAction(req, clientIP) - return req, nil - } else if jsres != nil { - // a fake response has been returned by the script - res := jsres.ToQuery() - p.logResponseAction(res, clientIP) - return query, res + // run the module OnRequest callback if defined + jsreq, jsres := p.Script.OnRequest(query, clientIP) + if jsreq != nil { + // the request has been changed by the script + req := jsreq.ToQuery() + p.logRequestAction(req, clientIP) + return req, nil + } else if jsres != nil { + // a fake response has been returned by the script + res := jsres.ToQuery() + p.logResponseAction(res, clientIP) + return query, res + } } return query, nil } func (p *DNSProxy) onResponseFilter(req, res *dns.Msg, clientIP string) *dns.Msg { - // sometimes it happens ¯\_(ツ)_/¯ - if res == nil { - return nil - } + if p.shouldProxy(clientIP) { + // sometimes it happens ¯\_(ツ)_/¯ + if res == nil { + return nil + } - p.Debug("> %s q[%s] a[%s] e[%s] n[%s]", - clientIP, - strings.Join(questionsToStrings(res.Question), ","), - strings.Join(recordsToStrings(res.Answer), ","), - strings.Join(recordsToStrings(res.Extra), ","), - strings.Join(recordsToStrings(res.Ns), ",")) + p.Debug("> %s q[%s] a[%s] e[%s] n[%s]", + clientIP, + strings.Join(questionsToStrings(res.Question), ","), + strings.Join(recordsToStrings(res.Answer), ","), + strings.Join(recordsToStrings(res.Extra), ","), + strings.Join(recordsToStrings(res.Ns), ",")) - // do we have a proxy script? - if p.Script != nil { - _, jsres := p.Script.OnResponse(req, res, clientIP) - if jsres != nil { - // the response has been changed by the script - res := jsres.ToQuery() - p.logResponseAction(res, clientIP) - return res + // do we have a proxy script? + if p.Script != nil { + _, jsres := p.Script.OnResponse(req, res, clientIP) + if jsres != nil { + // the response has been changed by the script + res := jsres.ToQuery() + p.logResponseAction(res, clientIP) + return res + } } } From ccb2774814e944dbfe0f444a9c6ecc18aea34b61 Mon Sep 17 00:00:00 2001 From: buffermet <29265684+buffermet@users.noreply.github.com> Date: Sat, 12 Oct 2024 21:50:26 +0200 Subject: [PATCH 08/10] Shrink code. --- modules/dns_proxy/dns_proxy_base_filters.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/modules/dns_proxy/dns_proxy_base_filters.go b/modules/dns_proxy/dns_proxy_base_filters.go index 60cdd5dd..48af3675 100644 --- a/modules/dns_proxy/dns_proxy_base_filters.go +++ b/modules/dns_proxy/dns_proxy_base_filters.go @@ -29,16 +29,12 @@ func tabsToSpaces(s string) string { } func (p *DNSProxy) logRequestAction(m *dns.Msg, clientIP string) { - var questions []string - for _, q := range m.Question { - questions = append(questions, tabsToSpaces(q.String())) - } p.Sess.Events.Add(p.Name+".spoofed-request", struct { Client string Questions []string }{ clientIP, - questions, + questionsToStrings(m.Question), }) } From 6de6de741855423a3ea28aa42d0122ece273f85c Mon Sep 17 00:00:00 2001 From: buffermet <29265684+buffermet@users.noreply.github.com> Date: Sat, 12 Oct 2024 22:05:29 +0200 Subject: [PATCH 09/10] Allow wildcard in blacklist. --- modules/dns_proxy/dns_proxy.go | 2 +- modules/dns_proxy/dns_proxy_base.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/dns_proxy/dns_proxy.go b/modules/dns_proxy/dns_proxy.go index 76de6db9..482e3aeb 100644 --- a/modules/dns_proxy/dns_proxy.go +++ b/modules/dns_proxy/dns_proxy.go @@ -113,7 +113,7 @@ func NewDnsProxy(s *session.Session) *DnsProxy { "Address to bind the DNS proxy to.")) mod.AddParam(session.NewStringParameter("dns.proxy.blacklist", "", "", - "Comma separated list of client IPs to skip while proxying.")) + "Comma separated list of client IPs to skip while proxying (wildcard allowed).")) mod.AddParam(session.NewStringParameter("dns.proxy.whitelist", "", "", "Comma separated list of client IPs to proxy if the blacklist is used.")) diff --git a/modules/dns_proxy/dns_proxy_base.go b/modules/dns_proxy/dns_proxy_base.go index ac637bf3..f8c17445 100644 --- a/modules/dns_proxy/dns_proxy_base.go +++ b/modules/dns_proxy/dns_proxy_base.go @@ -51,7 +51,7 @@ func (p *DNSProxy) shouldProxy(clientIP string) bool { // check if this client is in the blacklist for _, ip := range p.Blacklist { - if clientIP == ip { + if ip == "*" || clientIP == ip { return false } } From 08e248e38c0407105c69db12ad6b6348151c2d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=B8=EF=B8=8F?= <29265684+buffermet@users.noreply.github.com> Date: Wed, 23 Oct 2024 13:30:01 +0200 Subject: [PATCH 10/10] Implement addSessionEvent function in DNS proxy script env --- modules/dns_proxy/dns_proxy_script.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/modules/dns_proxy/dns_proxy_script.go b/modules/dns_proxy/dns_proxy_script.go index aa2c9d5f..4a608168 100644 --- a/modules/dns_proxy/dns_proxy_script.go +++ b/modules/dns_proxy/dns_proxy_script.go @@ -1,6 +1,8 @@ package dns_proxy import ( + "strings" + "github.com/bettercap/bettercap/v2/log" "github.com/bettercap/bettercap/v2/session" "github.com/evilsocket/islazy/plugin" @@ -32,6 +34,31 @@ func LoadDnsProxyScript(path string, sess *session.Session) (err error, s *DnsPr return } + // define addSessionEvent function + err = plug.Set("addSessionEvent", func(call otto.FunctionCall) otto.Value { + if len(call.ArgumentList) < 2 { + log.Error("Failed to execute 'addSessionEvent' in DNS proxy: 2 arguments required, but only %d given.", len(call.ArgumentList)) + return otto.FalseValue() + } + ottoTag := call.Argument(0) + if !ottoTag.IsString() { + log.Error("Failed to execute 'addSessionEvent' in DNS proxy: first argument must be a string.") + return otto.FalseValue() + } + tag := strings.TrimSpace(ottoTag.String()) + if tag == "" { + log.Error("Failed to execute 'addSessionEvent' in DNS proxy: tag cannot be empty.") + return otto.FalseValue() + } + data := call.Argument(1) + sess.Events.Add(tag, data) + return otto.TrueValue() + }) + if err != nil { + log.Error("Error while defining addSessionEvent function: %+v", err) + return + } + // run onLoad if defined if plug.HasFunc("onLoad") { if _, err = plug.Call("onLoad"); err != nil {