Skip to content

Commit 76adb90

Browse files
committed
Parallelize DNS record lookups
Support using TCP or TCP with TLS for DNS queries Fix incorrect logger injection for the scanner Increase default DNS buffer from `1024` to `4096` Strip unneeded information from release builds
1 parent 5a7486b commit 76adb90

10 files changed

Lines changed: 127 additions & 78 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
mkdir -p builds/compressed
2020
go install github.com/mitchellh/gox@latest
2121
cd cmd/dss
22-
gox --output "../../builds/dss-{{.OS}}-{{.Arch}}" -osarch 'darwin/amd64 darwin/arm64 linux/amd64 linux/arm freebsd/amd64 windows/amd64'
22+
gox --output "../../builds/dss-{{.OS}}-{{.Arch}}" -ldflags '-s -w' -osarch 'darwin/amd64 darwin/arm64 linux/amd64 linux/arm freebsd/amd64 windows/amd64'
2323
cd ../../builds
2424
find . -maxdepth 1 -type f -execdir zip 'compressed/{}.zip' '{}' \;
2525
- name: Upload Binaries

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,8 @@ You can then email this inbox from any address, and you'll receive an email back
207207
| `--concurrent` | `-c` | The number of domains to scan concurrently (defaults to your number of CPU threads) |
208208
| `--debug` | `-d` | Print debug logs |
209209
| `--dkimSelector` | | Specify a comma seperated list of DKIM selectors (default "") |
210-
| `--dnsBuffer` | | Specify the allocated buffer for DNS responses (default 1024) |
210+
| `--dnsBuffer` | | Specify the allocated buffer for DNS responses (default 4096) |
211+
| `--dnsProtocol` | | Use udp, tcp, or tcp-tls for DNS queries (default udp) |
211212
| `--format` | `-f` | Format to print results in (yaml, json, csv) (default "yaml") |
212213
| `--nameservers` | `-n` | Use specific nameservers, in host[:port] format; may be specified multiple times |
213214
| `--outputFile` | `-o` | Output the results to a specified file (creates a file with the current unix timestamp if no file is specified) |

cmd/dss/main.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ var (
2525
Use: "dss",
2626
Short: "Scan a domain's DNS records.",
2727
Long: "Scan a domain's DNS records.\nhttps://github.com/GlobalCyberAlliance/domain-security-scanner",
28-
Version: "3.0.2",
28+
Version: "3.0.3",
2929
PersistentPreRun: func(cmd *cobra.Command, args []string) {
3030
if debug {
3131
log = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}).With().Timestamp().Logger().Level(zerolog.DebugLevel)
@@ -58,7 +58,7 @@ var (
5858
cfg *Config
5959
log zerolog.Logger
6060
writeToFileCounter int
61-
format, outputFile string
61+
dnsProtocol, format, outputFile string
6262
dkimSelector, nameservers []string
6363
advise, debug, checkTLS, zoneFile bool
6464
dnsBuffer uint16
@@ -73,7 +73,8 @@ func main() {
7373
cmd.PersistentFlags().Uint16VarP(&concurrent, "concurrent", "c", uint16(runtime.NumCPU()), "The number of domains to scan concurrently")
7474
cmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "Print debug logs")
7575
cmd.PersistentFlags().StringSliceVar(&dkimSelector, "dkimSelector", []string{}, "Specify a DKIM selector")
76-
cmd.PersistentFlags().Uint16Var(&dnsBuffer, "dnsBuffer", 1024, "Specify the allocated buffer for DNS responses")
76+
cmd.PersistentFlags().Uint16Var(&dnsBuffer, "dnsBuffer", 4096, "Specify the allocated buffer for DNS responses")
77+
cmd.PersistentFlags().StringVar(&dnsProtocol, "dnsProtocol", "udp", "Use udp, tcp, or tcp-tls for DNS queries")
7778
cmd.PersistentFlags().StringVarP(&format, "format", "f", "yaml", "Format to print results in (yaml, json)")
7879
cmd.PersistentFlags().StringSliceVarP(&nameservers, "nameservers", "n", nil, "Use specific nameservers, in `host[:port]` format; may be specified multiple times")
7980
cmd.PersistentFlags().StringVarP(&outputFile, "outputFile", "o", "", "Output the results to a specified file (creates a file with the current unix timestamp if no file is specified)")

cmd/dss/scan.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"github.com/GlobalCyberAlliance/domain-security-scanner/pkg/advisor"
88
"github.com/GlobalCyberAlliance/domain-security-scanner/pkg/model"
99
"github.com/GlobalCyberAlliance/domain-security-scanner/pkg/scanner"
10-
"github.com/rs/zerolog"
1110
"github.com/spf13/cobra"
1211
)
1312

@@ -25,14 +24,15 @@ var cmdScan = &cobra.Command{
2524
scanner.WithCacheDuration(cache),
2625
scanner.WithConcurrentScans(concurrent),
2726
scanner.WithDNSBuffer(dnsBuffer),
27+
scanner.WithDNSProtocol(dnsProtocol),
2828
scanner.WithNameservers(nameservers),
2929
}
3030

3131
if len(dkimSelector) > 0 {
3232
opts = append(opts, scanner.WithDKIMSelectors(dkimSelector...))
3333
}
3434

35-
sc, err := scanner.New(zerolog.Logger{}, timeout, opts...)
35+
sc, err := scanner.New(log, timeout, opts...)
3636
if err != nil {
3737
log.Fatal().Err(err).Msg("An unexpected error occurred.")
3838
}

cmd/dss/serve.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"github.com/GlobalCyberAlliance/domain-security-scanner/pkg/http"
88
"github.com/GlobalCyberAlliance/domain-security-scanner/pkg/mail"
99
"github.com/GlobalCyberAlliance/domain-security-scanner/pkg/scanner"
10-
"github.com/rs/zerolog"
1110
"github.com/spf13/cobra"
1211
)
1312

@@ -52,14 +51,15 @@ var (
5251
scanner.WithCacheDuration(cache),
5352
scanner.WithConcurrentScans(concurrent),
5453
scanner.WithDNSBuffer(dnsBuffer),
54+
scanner.WithDNSProtocol(dnsProtocol),
5555
scanner.WithNameservers(nameservers),
5656
}
5757

5858
if len(dkimSelector) > 0 {
5959
opts = append(opts, scanner.WithDKIMSelectors(dkimSelector...))
6060
}
6161

62-
sc, err := scanner.New(zerolog.Logger{}, timeout, opts...)
62+
sc, err := scanner.New(log, timeout, opts...)
6363
if err != nil {
6464
log.Fatal().Err(err).Msg("could not create domain scanner")
6565
}
@@ -83,14 +83,15 @@ var (
8383
scanner.WithCacheDuration(cache),
8484
scanner.WithConcurrentScans(concurrent),
8585
scanner.WithDNSBuffer(dnsBuffer),
86+
scanner.WithDNSProtocol(dnsProtocol),
8687
scanner.WithNameservers(nameservers),
8788
}
8889

8990
if len(dkimSelector) > 0 {
9091
opts = append(opts, scanner.WithDKIMSelectors(dkimSelector...))
9192
}
9293

93-
sc, err := scanner.New(zerolog.Logger{}, timeout, opts...)
94+
sc, err := scanner.New(log, timeout, opts...)
9495
if err != nil {
9596
log.Fatal().Err(err).Msg("could not create domain scanner")
9697
}

pkg/advisor/advisor.go

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -69,53 +69,44 @@ func NewAdvisor(timeout time.Duration, cacheLifetime time.Duration, checkTLS boo
6969
return &advisor
7070
}
7171

72-
func (a *Advisor) CheckAll(domain string, bimi string, dkim string, dmarc string, mx []string, spf string) *Advice {
73-
var adviceDomain, adviceBIMI, adviceDKIM, adviceDMARC, adviceMX, adviceSPF []string
72+
func (a *Advisor) CheckAll(domain, bimi, dkim, dmarc string, mx []string, spf string) *Advice {
73+
advice := &Advice{}
7474
var wg sync.WaitGroup
7575

7676
wg.Add(6)
7777
go func() {
78-
adviceDomain = a.CheckDomain(domain)
78+
advice.Domain = a.CheckDomain(domain)
7979
wg.Done()
8080
}()
8181

8282
go func() {
83-
adviceBIMI = a.CheckBIMI(bimi)
83+
advice.BIMI = a.CheckBIMI(bimi)
8484
wg.Done()
8585
}()
8686

8787
go func() {
88-
adviceDKIM = a.CheckDKIM(dkim)
88+
advice.DKIM = a.CheckDKIM(dkim)
8989
wg.Done()
9090
}()
9191

9292
go func() {
93-
adviceDMARC = a.CheckDMARC(dmarc)
93+
advice.DMARC = a.CheckDMARC(dmarc)
9494
wg.Done()
9595
}()
9696

9797
go func() {
98-
adviceMX = a.CheckMX(mx)
98+
advice.MX = a.CheckMX(mx)
9999
wg.Done()
100100
}()
101101

102102
go func() {
103-
adviceSPF = a.CheckSPF(spf)
103+
advice.SPF = a.CheckSPF(spf)
104104
wg.Done()
105105
}()
106106

107107
wg.Wait()
108108

109-
advice := Advice{
110-
Domain: adviceDomain,
111-
BIMI: adviceBIMI,
112-
DKIM: adviceDKIM,
113-
DMARC: adviceDMARC,
114-
MX: adviceMX,
115-
SPF: adviceSPF,
116-
}
117-
118-
return &advice
109+
return advice
119110
}
120111

121112
func (a *Advisor) CheckBIMI(bimi string) (advice []string) {

pkg/scanner/options.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"net"
66
"net/netip"
77
"runtime"
8+
"strings"
89
"time"
910

1011
"github.com/miekg/dns"
@@ -68,8 +69,8 @@ func WithDKIMSelectors(selectors ...string) Option {
6869
// WithDNSBuffer increases the allocated buffer for DNS responses
6970
func WithDNSBuffer(bufferSize uint16) Option {
7071
return func(s *Scanner) error {
71-
if bufferSize > 4096 {
72-
s.logger.Warn().Msg("buffer size should not be larger than 4096")
72+
if bufferSize <= 0 {
73+
return fmt.Errorf("invalid DNS buffer size: %d", bufferSize)
7374
}
7475

7576
s.dnsBuffer = bufferSize
@@ -78,6 +79,22 @@ func WithDNSBuffer(bufferSize uint16) Option {
7879
}
7980
}
8081

82+
// WithDNSProtocol sets the DNS protocol to use for queries.
83+
func WithDNSProtocol(protocol string) Option {
84+
return func(s *Scanner) error {
85+
protocol = strings.ToLower(protocol)
86+
87+
switch protocol {
88+
case "udp", "tcp", "tcp-tls":
89+
s.dnsClient.Net = protocol
90+
default:
91+
return fmt.Errorf("invalid DNS protocol: %s, valid options: udp, tcp, tcp-tls", protocol)
92+
}
93+
94+
return nil
95+
}
96+
}
97+
8198
// WithNameservers allows the caller to provide a custom set of nameservers for
8299
// a *Scanner to use. If ns is nil, or zero-length, the *Scanner will use
83100
// the nameservers specified in /etc/resolv.conf.

pkg/scanner/options_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,34 @@ func TestOptionWithDNSBuffer(t *testing.T) {
128128
})
129129
}
130130

131+
func TestOptionWithDNSProtocol(t *testing.T) {
132+
logger := zerolog.Nop()
133+
timeout := time.Second * 5
134+
135+
t.Run("InvalidProtocol", func(t *testing.T) {
136+
_, err := New(logger, timeout, WithDNSProtocol("invalid_protocol"))
137+
assert.Error(t, err)
138+
})
139+
140+
t.Run("ValidProtocolTCP", func(t *testing.T) {
141+
scanner, err := New(logger, timeout, WithDNSProtocol("TCP"))
142+
assert.NoError(t, err)
143+
assert.Equal(t, "tcp", scanner.dnsClient.Net)
144+
})
145+
146+
t.Run("ValidProtocolTCPWithTLS", func(t *testing.T) {
147+
scanner, err := New(logger, timeout, WithDNSProtocol("TCP-tls"))
148+
assert.NoError(t, err)
149+
assert.Equal(t, "tcp-tls", scanner.dnsClient.Net)
150+
})
151+
152+
t.Run("ValidProtocolUDP", func(t *testing.T) {
153+
scanner, err := New(logger, timeout, WithDNSProtocol("UDP"))
154+
assert.NoError(t, err)
155+
assert.Equal(t, "udp", scanner.dnsClient.Net)
156+
})
157+
}
158+
131159
func TestOptionWithNameservers(t *testing.T) {
132160
logger := zerolog.Nop()
133161
timeout := time.Second * 5

pkg/scanner/requests.go

Lines changed: 13 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"strings"
66

77
"github.com/miekg/dns"
8-
"github.com/pkg/errors"
98
)
109

1110
const (
@@ -60,17 +59,17 @@ func (s *Scanner) getDNSRecords(domain string, recordType uint16) (records []str
6059
answer.Header().Rrtype = recordType
6160
}
6261

63-
switch t := answer.(type) {
62+
switch dnsRec := answer.(type) {
6463
case *dns.A:
65-
records = append(records, t.A.String())
64+
records = append(records, dnsRec.A.String())
6665
case *dns.AAAA:
67-
records = append(records, t.AAAA.String())
66+
records = append(records, dnsRec.AAAA.String())
6867
case *dns.MX:
69-
records = append(records, t.Mx)
68+
records = append(records, dnsRec.Mx)
7069
case *dns.NS:
71-
records = append(records, t.Ns)
70+
records = append(records, dnsRec.Ns)
7271
case *dns.TXT:
73-
records = append(records, t.Txt...)
72+
records = append(records, dnsRec.Txt...)
7473
}
7574
}
7675

@@ -81,14 +80,20 @@ func (s *Scanner) getDNSRecords(domain string, recordType uint16) (records []str
8180
// It returns a slice of dns.RR (DNS resource records) and an error if any occurred.
8281
func (s *Scanner) getDNSAnswers(domain string, recordType uint16) ([]dns.RR, error) {
8382
req := &dns.Msg{}
84-
req.SetQuestion(dns.Fqdn(domain), recordType)
83+
req.Id = dns.Id()
84+
req.RecursionDesired = true
8585
req.SetEdns0(s.dnsBuffer, true) // increases the response buffer size
86+
req.SetQuestion(dns.Fqdn(domain), recordType)
8687

8788
in, _, err := s.dnsClient.Exchange(req, s.getNS())
8889
if err != nil {
8990
return nil, err
9091
}
9192

93+
if in.Rcode != dns.RcodeSuccess {
94+
return nil, fmt.Errorf("DNS query failed with rcode %v", in.Rcode)
95+
}
96+
9297
if in.MsgHdr.Truncated && s.dnsBuffer < 4096 {
9398
s.logger.Warn().Msg(fmt.Sprintf("DNS buffer %v was too small for %v, retrying with larger buffer (4096)", s.dnsBuffer, domain))
9499

@@ -103,35 +108,6 @@ func (s *Scanner) getDNSAnswers(domain string, recordType uint16) ([]dns.RR, err
103108
return in.Answer, nil
104109
}
105110

106-
// GetDNSRecords is a convenience wrapper which will scan all provided DNS record types
107-
// and fill the pointered ScanResult. It returns an error if any occurred.
108-
func (s *Scanner) GetDNSRecords(scanResult *Result, recordTypes ...string) (err error) {
109-
for _, recordType := range recordTypes {
110-
switch strings.ToUpper(recordType) {
111-
case "BIMI":
112-
scanResult.BIMI, err = s.getTypeBIMI(scanResult.Domain)
113-
case "DKIM":
114-
scanResult.DKIM, err = s.getTypeDKIM(scanResult.Domain)
115-
case "DMARC":
116-
scanResult.DMARC, err = s.getTypeDMARC(scanResult.Domain)
117-
case "MX":
118-
scanResult.MX, err = s.getDNSRecords(scanResult.Domain, dns.TypeMX)
119-
case "NS":
120-
scanResult.NS, err = s.getDNSRecords(scanResult.Domain, dns.TypeNS)
121-
case "SPF":
122-
scanResult.SPF, err = s.getTypeSPF(scanResult.Domain)
123-
default:
124-
return errors.New("invalid dns record type")
125-
}
126-
127-
if err != nil {
128-
return err
129-
}
130-
}
131-
132-
return nil
133-
}
134-
135111
func (s *Scanner) getTypeBIMI(domain string) (string, error) {
136112
for _, dname := range []string{
137113
"default._bimi." + domain,

0 commit comments

Comments
 (0)