Repository: zmap/zdns
Pull request #427 (2024-08-20 20:16:16)

Move ZDNS to using ZFlags instead of Cobra
## Description
ZFlags should give us more flexibility (we can now use short form flags (`-t`!)) and make ZDNS act more similar to ZGrab. Additionally, it should make having multiple modules as requested #234 possible, similar to how it's done in ZGrab.

Version Flag (`-v`, `--version`)
```
$ ./zdns -v
zdns version 1.1.0
```

New `--help`
```
$ ./zdns -h
Usage:
  zdns [OPTIONS] <command>

ZDNS is a library and CLI tool for making very fast DNS requests. It's built upon
https://github.com/zmap/dns (and in turn https://github.com/miekg/dns) for constructing
and parsing raw DNS packets.
ZDNS also includes its own recursive resolution and a cache to further optimize performance.

General Options:
      --all-nameservers        Perform the lookup via all the nameservers for the domain.
      --cache-size=            how many items can be stored in internal recursive cache (default: 10000)
      --go-processes=          number of OS processes (GOMAXPROCS by default) (default: 0)
      --iteration-timeout=     timeout for a single iterative step in an iterative query, in seconds. Only applicable with --iterative (default: 4)
      --iterative              Perform own iteration instead of relying on recursive resolver
      --max-depth=             how deep should we recurse when performing iterative lookups (default: 10)
      --name-server-mode       Treats input as nameservers to query with a static query rather than queries to send to a static name server
      --name-servers=          List of DNS servers to use. Can be passed as comma-delimited string or via @/path/to/file. If no port is specified, defaults to 53.
      --nanoseconds            Use nanosecond resolution timestamps in output
      --no-follow-cnames       do not follow CNAMEs/DNAMEs in the lookup process
      --retries=               how many times should zdns retry query if timeout or temporary failure (default: 1)
  -t, --threads=               number of lightweight go threads (default: 1000)
      --timeout=               timeout for resolving a individual name, in seconds (default: 15)
  -v, --version                Print the version of zdns and exit

Query Options:
      --checking-disabled      Sends DNS packets with the CD bit set
      --class=                 DNS class to query. Options: INET, CSNET, CHAOS, HESIOD, NONE, ANY. (default: INET)
      --client-subnet=         Client subnet in CIDR format for EDNS0.
      --dnssec                 Requests DNSSEC records by setting the DNSSEC OK (DO) bit
      --nsid                   Request NSID.

Network Options:
      --4                      utilize IPv4 query transport only, incompatible with --6
      --6                      utilize IPv6 query transport only, incompatible with --4
      --local-addr=            comma-delimited list of local addresses to use, serve as the source IP for outbound queries
      --local-interface=       local interface to use
      --no-recycle-sockets     do not create long-lived unbound UDP socket for each thread at launch and reuse for all (UDP) queries
      --prefer-ipv4-iteration  Prefer IPv4/A record lookups during iterative resolution. Ignored unless used with both IPv4 and IPv6 query transport
      --prefer-ipv6-iteration  Prefer IPv6/AAAA record lookups during iterative resolution. Ignored unless used with both IPv4 and IPv6 query transport
      --tcp-only               Only perform lookups over TCP
      --udp-only               Only perform lookups over UDP

Input/Output Options:
      --alexa                  is input file from Alexa Top Million download
      --blacklist-file=        blacklist file for servers to exclude from lookups
      --conf-file=             config file for DNS servers (default: /etc/resolv.conf)
      --include-fields=        Comma separated list of fields to additionally output beyond result verbosity. Options: class, protocol, ttl, resolver, flags
  -f, --input-file=            names to read, defaults to stdin (default: -)
      --log-file=              where should JSON logs be saved, defaults to stderr (default: -)
      --metadata-file=         where should JSON metadata be saved, defaults to no metadata output. Use '-' for stderr.
      --metadata-passthrough   if input records have the form 'name,METADATA', METADATA will be propagated to the output
  -o, --output-file=           where should JSON output be saved, defaults to stdout (default: -)
      --override-name=         name overrides all passed in names. Commonly used with --name-server-mode.
      --prefix=                name to be prepended to what's passed in (e.g., www.)
      --result-verbosity=      Sets verbosity of each output record. Options: short, normal, long, trace (default: normal)
      --verbosity=             log verbosity: 1 (lowest)--5 (highest) (default: 3)

Help Options:
  -h, --help                   Show this help message


Available commands:
  A
  AAAA
  AFSDB
  ALOOKUP
  ANY
  ATMA
  AVC
  AXFR
  BINDVERSION
  CAA
  CDNSKEY
  CDS
  CERT
  CNAME
  CSYNC
  DHCID
  DMARC
  DNAME
  DNSKEY
  DS
  EID
  EUI48
  EUI64
  GID
  GPOS
  HINFO
  HIP
  HTTPS
  ISDN
  KEY
  KX
  L32
  L64
  LOC
  LP
  MB
  MD
  MF
  MG
  MR
  MX
  MXLOOKUP
  NAPTR
  NID
  NIMLOC
  NINFO
  NS
  NSAPPTR
  NSEC
  NSEC3
  NSEC3PARAM
  NSLOOKUP
  NULL
  NXT
  OPENPGPKEY
  PTR
  PX
  RP
  RRSIG
  RT
  SMIMEA
  SOA
  SPF
  SPF
  SRV
  SSHFP
  SVCB
  TALINK
  TKEY
  TLSA
  TXT
  UID
  UINFO
  UNSPEC
  URI
```

Module-Specific Help
```
$ ./zdns alookup --help
Usage:
  zdns [OPTIONS] ALOOKUP [ALOOKUP-OPTIONS]

alookup will get the information that is typically desired, instead of just the information that exists in a single record. Specifically, alookup acts similar to nslookup and will follow CNAME records.

General Options:
      --all-nameservers        Perform the lookup via all the nameservers for the domain.
      --cache-size=            how many items can be stored in internal recursive cache (default: 10000)
      --go-processes=          number of OS processes (GOMAXPROCS by default) (default: 0)
      --iteration-timeout=     timeout for a single iterative step in an iterative query, in seconds. Only applicable with --iterative (default: 4)
      --iterative              Perform own iteration instead of relying on recursive resolver
      --max-depth=             how deep should we recurse when performing iterative lookups (default: 10)
      --name-server-mode       Treats input as nameservers to query with a static query rather than queries to send to a static name server
      --name-servers=          List of DNS servers to use. Can be passed as comma-delimited string or via @/path/to/file. If no port is specified, defaults to 53.
      --nanoseconds            Use nanosecond resolution timestamps in output
      --no-follow-cnames       do not follow CNAMEs/DNAMEs in the lookup process
      --retries=               how many times should zdns retry query if timeout or temporary failure (default: 1)
  -t, --threads=               number of lightweight go threads (default: 1000)
      --timeout=               timeout for resolving a individual name, in seconds (default: 15)
  -v, --version                Print the version of zdns and exit

Query Options:
      --checking-disabled      Sends DNS packets with the CD bit set
      --class=                 DNS class to query. Options: INET, CSNET, CHAOS, HESIOD, NONE, ANY. (default: INET)
      --client-subnet=         Client subnet in CIDR format for EDNS0.
      --dnssec                 Requests DNSSEC records by setting the DNSSEC OK (DO) bit
      --nsid                   Request NSID.

Network Options:
      --4                      utilize IPv4 query transport only, incompatible with --6
      --6                      utilize IPv6 query transport only, incompatible with --4
      --local-addr=            comma-delimited list of local addresses to use, serve as the source IP for outbound queries
      --local-interface=       local interface to use
      --no-recycle-sockets     do not create long-lived unbound UDP socket for each thread at launch and reuse for all (UDP) queries
      --prefer-ipv4-iteration  Prefer IPv4/A record lookups during iterative resolution. Ignored unless used with both IPv4 and IPv6 query transport
      --prefer-ipv6-iteration  Prefer IPv6/AAAA record lookups during iterative resolution. Ignored unless used with both IPv4 and IPv6 query transport
      --tcp-only               Only perform lookups over TCP
      --udp-only               Only perform lookups over UDP

Input/Output Options:
      --alexa                  is input file from Alexa Top Million download
      --blacklist-file=        blacklist file for servers to exclude from lookups
      --conf-file=             config file for DNS servers (default: /etc/resolv.conf)
      --include-fields=        Comma separated list of fields to additionally output beyond result verbosity. Options: class, protocol, ttl, resolver, flags
  -f, --input-file=            names to read, defaults to stdin (default: -)
      --log-file=              where should JSON logs be saved, defaults to stderr (default: -)
      --metadata-file=         where should JSON metadata be saved, defaults to no metadata output. Use '-' for stderr.
      --metadata-passthrough   if input records have the form 'name,METADATA', METADATA will be propagated to the output
  -o, --output-file=           where should JSON output be saved, defaults to stdout (default: -)
      --override-name=         name overrides all passed in names. Commonly used with --name-server-mode.
      --prefix=                name to be prepended to what's passed in (e.g., www.)
      --result-verbosity=      Sets verbosity of each output record. Options: short, normal, long, trace (default: normal)
      --verbosity=             log verbosity: 1 (lowest)--5 (highest) (default: 3)

Help Options:
  -h, --help                   Show this help message

[ALOOKUP command options]
          --ipv4-lookup        perform A lookups for each server
          --ipv6-lookup        perform AAAA lookups for each server

```


## Testing
### Normal Usage
```
$ echo "cloudflare.com" | ./zdns A
{"data":{"additionals":[{"flags":"","type":"EDNS0","udpsize":512,"version":0}],"answers":[{"answer":"104.16.133.229","class":"IN","name":"cloudflare.com","ttl":164,"type":"A"},{"answer":"104.16.132.229","class":"IN","name":"cloudflare.com","ttl":164,"type":"A"}],"protocol":"udp","resolver":"[2603:6013:9d00:3302::1]:53"},"duration":0.04202225,"name":"cloudflare.com","status":"NOERROR","timestamp":"2024-08-20T16:13:44-04:00"}
```
### Using a flag
```
$ echo "cloudflare.com" | ./zdns A --verbosity=5
INFO[0000] No iteration IP preference specified, defaulting to IPv4 preferred. See --prefer-ipv4-iteration and --prefer-ipv6-iteration for more info
INFO[0000] using local addresses: [192.168.1.243 2603:6013:9d00:3302:c460:464e:f5c0:9930]
INFO[0000] for non-iterative lookups, using external nameservers: 192.168.1.1:53, [2603:6013:9d00:3302::1]:53
INFO[0000] for iterative lookups, using nameservers: 192.168.1.1:53, [2603:6013:9d00:3302::1]:53
INFO[0000] no name server provided for external lookup, using  random external name server: [2603:6013:9d00:3302::1]:53
DEBU[0000] DEPTH 00:[MIEKG-IN: starting a C/DNAME following lookup for  cloudflare.com  ( 1 )]
DEBU[0000] DEPTH 01:    [MIEKG-IN: following external lookup for  cloudflare.com  ( 1 )]
DEBU[0000] DEPTH 01:    [****WIRE LOOKUP***  A   cloudflare.com   [2603:6013:9d00:3302::1]:53]
DEBU[0000] DEPTH 01:    [MIEKG-OUT: following external lookup for  cloudflare.com  ( 1 ) with  1  attempts: status:  NOERROR  , err:  <nil>]
{"data":{"additionals":[{"flags":"","type":"EDNS0","udpsize":4096,"version":0}],"answers":[{"answer":"104.16.133.229","class":"IN","name":"cloudflare.com","ttl":153,"type":"A"},{"answer":"104.16.132.229","class":"IN","name":"cloudflare.com","ttl":153,"type":"A"}],"protocol":"udp","resolver":"[2603:6013:9d00:3302::1]:53"},"duration":0.019543083,"name":"cloudflare.com","status":"NOERROR","timestamp":"2024-08-20T16:13:55-04:00"}
```
### Using a flag before a module
```
$ make && echo "cloudflare.com" | ./zdns --verbosity 5 alookup       
go build -o zdns
INFO[0000] No iteration IP preference specified, defaulting to IPv4 preferred. See --prefer-ipv4-iteration and --prefer-ipv6-iteration for more info 
INFO[0000] using local addresses: [192.168.1.243 2603:6013:9d00:3302:c460:464e:f5c0:9930] 
INFO[0000] for non-iterative lookups, using external nameservers: 192.168.1.1:53, [2603:6013:9d00:3302::1]:53 
INFO[0000] for iterative lookups, using nameservers: 192.168.1.1:53, [2603:6013:9d00:3302::1]:53 
INFO[0000] no name server provided for external lookup, using  random external name server: [2603:6013:9d00:3302::1]:53 
DEBU[0000] DEPTH 00:[MIEKG-IN: starting a C/DNAME following lookup for  cloudflare.com  ( 1 )] 
DEBU[0000] DEPTH 01:    [MIEKG-IN: following external lookup for  cloudflare.com  ( 1 )] 
DEBU[0000] DEPTH 01:    [****WIRE LOOKUP***  A   cloudflare.com   [2603:6013:9d00:3302::1]:53] 
DEBU[0000] DEPTH 01:    [MIEKG-OUT: following external lookup for  cloudflare.com  ( 1 ) with  1  attempts: status:  NOERROR  , err:  <nil>] 
{"data":{"ipv4_addresses":["104.16.133.229","104.16.132.229"]},"duration":0.037664875,"name":"cloudflare.com","status":"NOERROR","timestamp":"2024-08-21T12:15:43-04:00"}
```
### Using a special module that adds extra flags
```
$ echo "cloudflare.com" | ./zdns nslookup --ipv4-lookup
{"data":{"servers":[{"ipv4_addresses":["162.159.5.6","162.159.3.11"],"name":"ns6.cloudflare.com","ttl":86400,"type":"NS"},{"ipv4_addresses":["162.159.4.8","162.159.6.6"],"name":"ns7.cloudflare.com","ttl":86400,"type":"NS"},{"ipv4_addresses":["162.159.0.33","162.159.7.226"],"name":"ns3.cloudflare.com","ttl":86400,"type":"NS"},{"ipv4_addresses":["162.159.8.55","162.159.1.33"],"name":"ns4.cloudflare.com","ttl":86400,"type":"NS"},{"ipv4_addresses":["162.159.9.55","162.159.2.9"],"name":"ns5.cloudflare.com","ttl":86400,"type":"NS"}]},"duration":0.231573292,"name":"cloudflare.com","status":"NOERROR","timestamp":"2024-08-20T16:15:29-04:00"}

~/zdns on  phillip/zflags-cli! ⌚ 16:15:29
$ echo "cloudflare.com" | ./zdns nslookup --ipv6-lookup
{"data":{"servers":[{"ipv6_addresses":["2400:cb00:2049:1::a29f:209","2400:cb00:2049:1::a29f:937"],"name":"ns5.cloudflare.com","ttl":86395,"type":"NS"},{"ipv6_addresses":["2400:cb00:2049:1::a29f:506","2400:cb00:2049:1::a29f:30b"],"name":"ns6.cloudflare.com","ttl":86395,"type":"NS"},{"ipv6_addresses":["2400:cb00:2049:1::a29f:408","2400:cb00:2049:1::a29f:606"],"name":"ns7.cloudflare.com","ttl":86395,"type":"NS"},{"ipv6_addresses":["2400:cb00:2049:1::a29f:7e2","2400:cb00:2049:1::a29f:21"],"name":"ns3.cloudflare.com","ttl":86395,"type":"NS"},{"ipv6_addresses":["2400:cb00:2049:1::a29f:837","2400:cb00:2049:1::a29f:121"],"name":"ns4.cloudflare.com","ttl":86395,"type":"NS"}]},"duration":0.331910166,"name":"cloudflare.com","status":"NOERROR","timestamp":"2024-08-20T16:15:34-04:00"}
```


Patch:
diff --git a/go.mod b/go.mod
index 271efe5..3338b67 100644
--- a/go.mod
+++ b/go.mod
@@ -6,45 +6,29 @@ require (
 	github.com/hashicorp/go-version v1.7.0
 	github.com/liip/sheriff v0.12.0
 	github.com/pkg/errors v0.9.1
+	github.com/schollz/progressbar/v3 v3.14.4
 	github.com/sirupsen/logrus v1.9.3
-	github.com/spf13/cobra v1.8.1
-	github.com/spf13/pflag v1.0.5
-	github.com/spf13/viper v1.19.0
 	github.com/stretchr/testify v1.9.0
 	github.com/zmap/dns v1.1.59-zdns-4
 	github.com/zmap/go-iptree v0.0.0-20210731043055-d4e632617837
+	github.com/zmap/zflags v1.4.0-beta.1.0.20200204220219-9d95409821b6
 	gotest.tools/v3 v3.5.1
 )
 
 require (
 	github.com/asergeyev/nradix v0.0.0-20220715161825-e451993e425c // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
-	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/google/go-cmp v0.6.0 // indirect
-	github.com/hashicorp/hcl v1.0.0 // indirect
-	github.com/inconshreveable/mousetrap v1.1.0 // indirect
-	github.com/magiconair/properties v1.8.7 // indirect
+	github.com/kr/pretty v0.3.1 // indirect
 	github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
-	github.com/mitchellh/mapstructure v1.5.0 // indirect
-	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
-	github.com/sagikazarmark/locafero v0.6.0 // indirect
-	github.com/sagikazarmark/slog-shim v0.1.0 // indirect
-	github.com/schollz/progressbar/v3 v3.14.4 // indirect
-	github.com/sourcegraph/conc v0.3.0 // indirect
-	github.com/spf13/afero v1.11.0 // indirect
-	github.com/spf13/cast v1.6.0 // indirect
-	github.com/subosito/gotenv v1.6.0 // indirect
-	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect
 	golang.org/x/mod v0.18.0 // indirect
 	golang.org/x/net v0.26.0 // indirect
 	golang.org/x/sync v0.7.0 // indirect
 	golang.org/x/sys v0.22.0 // indirect
 	golang.org/x/term v0.22.0 // indirect
-	golang.org/x/text v0.16.0 // indirect
 	golang.org/x/tools v0.22.0 // indirect
-	gopkg.in/ini.v1 v1.67.0 // indirect
+	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 117bc00..589cc42 100644
--- a/go.sum
+++ b/go.sum
@@ -1,36 +1,26 @@
 github.com/asergeyev/nradix v0.0.0-20170505151046-3872ab85bb56/go.mod h1:8BhOLuqtSuT5NZtZMwfvEibi09RO3u79uqfHZzfDTR4=
 github.com/asergeyev/nradix v0.0.0-20220715161825-e451993e425c h1:cN6WRmhJkh/u5bvf/XXjoqcHxljVKIz3Nt7q2dVJySo=
 github.com/asergeyev/nradix v0.0.0-20220715161825-e451993e425c/go.mod h1:8BhOLuqtSuT5NZtZMwfvEibi09RO3u79uqfHZzfDTR4=
-github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
-github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
-github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
 github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
-github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
-github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
-github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/liip/sheriff v0.12.0 h1:/deCBoWPcsG5rN/NlHIwNjnj8/U4bN99NdWY8OmQ6RA=
 github.com/liip/sheriff v0.12.0/go.mod h1:TgdKuCGI/1qkwBjm96/hEN+gD2FEHf8is9YaqGq96o8=
-github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
-github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
 github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
-github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
-github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
-github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -39,48 +29,22 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
-github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
-github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
-github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
-github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/schollz/progressbar/v3 v3.14.4 h1:W9ZrDSJk7eqmQhd3uxFNNcTr0QL+xuGNI9dEMrw0r74=
 github.com/schollz/progressbar/v3 v3.14.4/go.mod h1:aT3UQ7yGm+2ZjeXPqsjTenwL3ddUiuZ0kfQ/2tHlyNI=
 github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
-github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
-github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
-github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
-github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
-github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
-github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
-github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
-github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
-github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
 github.com/zmap/dns v1.1.59-zdns-4 h1:4pSRyypzrX7xeq63vXc5kbjvqbcJNNrjEb2dyOREBeU=
 github.com/zmap/dns v1.1.59-zdns-4/go.mod h1:8/9bo3vXwN6eg8Eu0t5OvBqqUXaG8950NL+2l130s2o=
 github.com/zmap/go-iptree v0.0.0-20210731043055-d4e632617837 h1:DjHnADS2r2zynZ3WkCFAQ+PNYngMSNceRROi0pO6c3M=
 github.com/zmap/go-iptree v0.0.0-20210731043055-d4e632617837/go.mod h1:9vp0bxqozzQwcjBwenEXfKVq8+mYbwHkQ1NF9Ap0DMw=
-go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
-go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM=
-golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
+github.com/zmap/zflags v1.4.0-beta.1.0.20200204220219-9d95409821b6 h1:XYA+NN2AS4mRmIDVu2nCtrjU17zKlRihO3MnlcmueUw=
+github.com/zmap/zflags v1.4.0-beta.1.0.20200204220219-9d95409821b6/go.mod h1:HXDUD+uue8yeLHr0eXx1lvY6CvMiHbTKw5nGmA9OUoo=
 golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
 golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
@@ -90,21 +54,16 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
-golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
 golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
 golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
 golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
-golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
-golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
 golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
 golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
-gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
-gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/src/cli/alookup.go b/src/cli/alookup.go
deleted file mode 100644
index 421d134..0000000
--- a/src/cli/alookup.go
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * ZDNS Copyright 2024 Regents of the University of Michigan
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy
- * of the License at http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- * implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package cli
-
-import (
-	"strings"
-
-	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
-
-	"github.com/zmap/zdns/src/internal/util"
-)
-
-// alookupCmd represents the alookup command
-var alookupCmd = &cobra.Command{
-	Use:   "alookup",
-	Short: "A record lookups that follow CNAME records",
-	Long: `alookup will get the information that is typically desired, instead of just
-the information that exists in a single record.
-
-Specifically, alookup acts similar to nslookup and will follow CNAME records.`,
-	Args: cobra.MatchAll(cobra.ExactArgs(0), cobra.OnlyValidArgs),
-	Run: func(cmd *cobra.Command, args []string) {
-		GC.Module = strings.ToUpper("alookup")
-		Run(GC, cmd.Flags())
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(alookupCmd)
-
-	alookupCmd.PersistentFlags().Bool("ipv4-lookup", false, "perform A lookups for each server")
-	alookupCmd.PersistentFlags().Bool("ipv6-lookup", false, "perform AAAA record lookups for each server")
-
-	util.BindFlags(alookupCmd, viper.GetViper(), util.EnvPrefix)
-}
diff --git a/src/cli/cli.go b/src/cli/cli.go
index 3cb9bef..68e3b58 100644
--- a/src/cli/cli.go
+++ b/src/cli/cli.go
@@ -11,27 +11,28 @@
  * implied. See the License for the specific language governing
  * permissions and limitations under the License.
  */
+
 package cli
 
 import (
+	"errors"
 	"fmt"
 	"net"
 	"os"
 	"strings"
 	"sync"
 
-	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
+	log "github.com/sirupsen/logrus"
 	"github.com/zmap/dns"
-
-	"github.com/zmap/zdns/src/internal/util"
-	"github.com/zmap/zdns/src/zdns"
+	flags "github.com/zmap/zflags"
 )
 
 const (
 	zdnsCLIVersion = "1.1.0"
 )
 
+var parser *flags.Parser
+
 type InputHandler interface {
 	FeedChannel(in chan<- string, wg *sync.WaitGroup) error
 }
@@ -39,178 +40,156 @@ type OutputHandler interface {
 	WriteResults(results <-chan string, wg *sync.WaitGroup) error
 }
 
-type CLIConf struct {
-	Threads             int
-	Timeout             int
-	IterationTimeout    int
-	Retries             int
-	AlexaFormat         bool
-	MetadataFormat      bool
-	IterativeResolution bool
-
-	NameServersString  string
-	LocalAddrString    string
-	LocalIfaceString   string
-	ConfigFilePath     string
-	ClassString        string
-	UseNanoseconds     bool
-	ClientSubnetString string
-
-	ResultVerbosity string
-	IncludeInOutput string
-	OutputGroups    []string
-
-	MaxDepth             int
-	CacheSize            int
-	GoMaxProcs           int
-	Verbosity            int
-	TimeFormat           string
-	NameServers          []string // recursive resolvers if not in iterative mode, root servers if in iterative mode
-	LookupAllNameServers bool
-	TCPOnly              bool
-	UDPOnly              bool
-	IPv4TransportOnly    bool // IPv4 transport only, incompatible with IPv6 transport only
-	IPv6TransportOnly    bool // IPv6 transport only, incompatible with IPv4 transport only
-	PreferIPv4Iteration  bool // Prefer IPv4/A record lookups during iterative resolution, only used if both IPv4 and IPv6 transport are enabled
-	PreferIPv6Iteration  bool // Prefer IPv6/AAAA record lookups during iterative resolution, only used if both IPv4 and IPv6 transport are enabled
-	RecycleSockets       bool
-	LocalAddrSpecified   bool
-	LocalAddrs           []net.IP
-	ClientSubnet         *dns.EDNS0_SUBNET
-	UseNSID              bool
-	Dnssec               bool
-	CheckingDisabled     bool
-
-	InputFilePath     string
-	OutputFilePath    string
-	BlacklistFilePath string
-	InputHandler      InputHandler
-	OutputHandler     OutputHandler
-	LogFilePath       string
-	MetadataFilePath  string
-
-	NamePrefix     string
-	NameOverride   string
-	NameServerMode bool
-	FollowCNAMEs   bool
-
-	Module string
-	Class  uint16
+// GeneralOptions core options for all ZDNS modules
+// Order here is the order they'll be printed to the user, so preserve alphabetical order
+type GeneralOptions struct {
+	LookupAllNameServers bool   `long:"all-nameservers" description:"Perform the lookup via all the nameservers for the domain."`
+	CacheSize            int    `long:"cache-size" default:"10000" description:"how many items can be stored in internal recursive cache"`
+	GoMaxProcs           int    `long:"go-processes" default:"0" description:"number of OS processes (GOMAXPROCS by default)"`
+	IterationTimeout     int    `long:"iteration-timeout" default:"4" description:"timeout for a single iterative step in an iterative query, in seconds. Only applicable with --iterative"`
+	IterativeResolution  bool   `long:"iterative" description:"Perform own iteration instead of relying on recursive resolver"`
+	MaxDepth             int    `long:"max-depth" default:"10" description:"how deep should we recurse when performing iterative lookups"`
+	NameServerMode       bool   `long:"name-server-mode" description:"Treats input as nameservers to query with a static query rather than queries to send to a static name server"`
+	NameServersString    string `long:"name-servers" description:"List of DNS servers to use. Can be passed as comma-delimited string or via @/path/to/file. If no port is specified, defaults to 53."`
+	UseNanoseconds       bool   `long:"nanoseconds" description:"Use nanosecond resolution timestamps in output"`
+	DisableFollowCNAMEs  bool   `long:"no-follow-cnames" description:"do not follow CNAMEs/DNAMEs in the lookup process"`
+	Retries              int    `long:"retries" default:"1" description:"how many times should zdns retry query if timeout or temporary failure"`
+	Threads              int    `short:"t" long:"threads" default:"1000" description:"number of lightweight go threads"`
+	Timeout              int    `long:"timeout" default:"15" description:"timeout for resolving a individual name, in seconds"`
+	Version              bool   `long:"version" short:"v" description:"Print the version of zdns and exit"`
 }
 
-var cfgFile string
-var GC CLIConf
+// QueryOptions affect the fields of the actual DNS queries. Applicable to all modules.
+type QueryOptions struct {
+	CheckingDisabled   bool   `long:"checking-disabled" description:"Sends DNS packets with the CD bit set"`
+	ClassString        string `long:"class" default:"INET" description:"DNS class to query. Options: INET, CSNET, CHAOS, HESIOD, NONE, ANY."`
+	ClientSubnetString string `long:"client-subnet" description:"Client subnet in CIDR format for EDNS0."`
+	Dnssec             bool   `long:"dnssec" description:"Requests DNSSEC records by setting the DNSSEC OK (DO) bit"`
+	UseNSID            bool   `long:"nsid" description:"Request NSID."`
+}
 
-// rootCmd represents the base command when called without any subcommands
-var rootCmd = &cobra.Command{
-	Use:   "zdns",
-	Short: "High-speed, low-drag DNS lookups",
-	Long: `ZDNS is a library and CLI tool for making very fast DNS requests. It's built upon
-https://github.com/zmap/dns (and in turn https://github.com/miekg/dns) for constructing
-and parsing raw DNS packets.
+// NetworkOptions options for controlling the network behavior. Applicable to all modules.
+type NetworkOptions struct {
+	IPv4TransportOnly     bool   `long:"4" description:"utilize IPv4 query transport only, incompatible with --6"`
+	IPv6TransportOnly     bool   `long:"6" description:"utilize IPv6 query transport only, incompatible with --4"`
+	LocalAddrString       string `long:"local-addr" description:"comma-delimited list of local addresses to use, serve as the source IP for outbound queries"`
+	LocalIfaceString      string `long:"local-interface" description:"local interface to use"`
+	DisableRecycleSockets bool   `long:"no-recycle-sockets" description:"do not create long-lived unbound UDP socket for each thread at launch and reuse for all (UDP) queries"`
+	PreferIPv4Iteration   bool   `long:"prefer-ipv4-iteration" description:"Prefer IPv4/A record lookups during iterative resolution. Ignored unless used with both IPv4 and IPv6 query transport"`
+	PreferIPv6Iteration   bool   `long:"prefer-ipv6-iteration" description:"Prefer IPv6/AAAA record lookups during iterative resolution. Ignored unless used with both IPv4 and IPv6 query transport"`
+	TCPOnly               bool   `long:"tcp-only" description:"Only perform lookups over TCP"`
+	UDPOnly               bool   `long:"udp-only" description:"Only perform lookups over UDP"`
+}
+
+// InputOutputOptions options for controlling the input and output behavior of zdns. Applicable to all modules.
+type InputOutputOptions struct {
+	AlexaFormat       bool   `long:"alexa" description:"is input file from Alexa Top Million download"`
+	BlacklistFilePath string `long:"blacklist-file" description:"blacklist file for servers to exclude from lookups"`
+	ConfigFilePath    string `long:"conf-file" default:"/etc/resolv.conf" description:"config file for DNS servers"`
+	IncludeInOutput   string `long:"include-fields" description:"Comma separated list of fields to additionally output beyond result verbosity. Options: class, protocol, ttl, resolver, flags"`
+	InputFilePath     string `short:"f" long:"input-file" default:"-" description:"names to read, defaults to stdin"`
+	LogFilePath       string `long:"log-file" default:"-" description:"where should JSON logs be saved, defaults to stderr"`
+	MetadataFilePath  string `long:"metadata-file" description:"where should JSON metadata be saved, defaults to no metadata output. Use '-' for stderr."`
+	MetadataFormat    bool   `long:"metadata-passthrough" description:"if input records have the form 'name,METADATA', METADATA will be propagated to the output"`
+	OutputFilePath    string `short:"o" long:"output-file" default:"-" description:"where should JSON output be saved, defaults to stdout"`
+	NameOverride      string `long:"override-name" description:"name overrides all passed in names. Commonly used with --name-server-mode."`
+	NamePrefix        string `long:"prefix" description:"name to be prepended to what's passed in (e.g., www.)"`
+	ResultVerbosity   string `long:"result-verbosity" default:"normal" description:"Sets verbosity of each output record. Options: short, normal, long, trace"`
+	Verbosity         int    `long:"verbosity" default:"3" description:"log verbosity: 1 (lowest)--5 (highest)"`
+}
 
-ZDNS also includes its own recursive resolution and a cache to further optimize performance.`,
-	ValidArgs: GetValidLookups(),
-	Args:      cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
-	Run: func(cmd *cobra.Command, args []string) {
-		GC.Module = strings.ToUpper(args[0])
-		Run(GC, cmd.Flags())
-	},
-	Version: zdnsCLIVersion,
+type CLIConf struct {
+	GeneralOptions
+	NetworkOptions
+	InputOutputOptions
+	QueryOptions
+	OutputGroups       []string
+	TimeFormat         string
+	NameServers        []string // recursive resolvers if not in iterative mode, root servers/servers to start iteration if in iterative mode
+	LocalAddrSpecified bool
+	LocalAddrs         []net.IP
+	ClientSubnet       *dns.EDNS0_SUBNET
+	InputHandler       InputHandler
+	OutputHandler      OutputHandler
+	Module             string
+	Class              uint16
 }
 
+var GC CLIConf
+
 // Execute adds all child commands to the root command and sets flags appropriately.
 // This is called by main.main(). It only needs to happen once to the rootCmd.
 func Execute() {
-	err := rootCmd.Execute()
+	parseArgs()
+	Run(GC)
+}
+
+// parseArgs parses the command line arguments and sets the global configuration
+// One limitation of the zflags library is you can't have "command-less" flags like ./zdns --version without turning
+// SubCommandsOptional = true. But then you don't get ZFlag's great command suggestion if you barely mistype a cmd.
+// Ex:./zdns AAAB -> Unknown command `AAAB', did you mean `AAAA'?
+// The below is a workaround to get the best of both worlds where we set SubcommandsOptiional to true, check for any
+// command-less flags, and then re-parse with SubcommandsOptional = false
+func parseArgs() {
+	if len(os.Args) >= 0 {
+		for i, arg := range os.Args {
+			// the below is necessary or else zdns --help is converted to zdns --HELP
+			if _, ok := GetValidLookups()[strings.ToUpper(arg)]; ok {
+				// we want users to be able to use `zdns nslookup` as well as `zdns NSLOOKUP`
+				os.Args[i] = strings.ToUpper(os.Args[i])
+				break // only capitalize the module, following strings could be domains
+			}
+
+		}
+	}
+	// setting this to true, only to get those flags that don't need a module (--version)
+	parser.SubcommandsOptional = true
+	parser.Options = flags.Default ^ flags.PrintErrors // we'll print errors in the 2nd invocation, otherwise we get the error printed twice
+	_, _, _, _ = parser.ParseCommandLine(os.Args[1:])
+	if GC.Version {
+		fmt.Printf("zdns version %s", zdnsCLIVersion)
+		fmt.Println()
+		os.Exit(0)
+	}
+	parser.SubcommandsOptional = false
+	parser.Options = flags.Default
+	_, moduleType, _, err := parser.ParseCommandLine(os.Args[1:])
 	if err != nil {
-		os.Exit(1)
+		var flagErr *flags.Error
+		if errors.As(err, &flagErr) {
+			// parser already printed error, exit without printing
+			os.Exit(1)
+		}
+		// exit and print
+		log.Fatal(err)
 	}
+	GC.Module = strings.ToUpper(moduleType)
 }
 
 func init() {
-	cobra.OnInitialize(initConfig)
-
-	// Here you will define your flags and configuration settings.
-	// Cobra supports persistent flags, which, if defined here,
-	// will be global for your application.
-
-	rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.zdns.yaml)")
-
-	// Cobra also supports local flags, which will only run
-	// when this action is called directly.
-	rootCmd.PersistentFlags().IntVar(&GC.Threads, "threads", 1000, "number of lightweight go threads")
-	rootCmd.PersistentFlags().IntVar(&GC.GoMaxProcs, "go-processes", 0, "number of OS processes (GOMAXPROCS)")
-	rootCmd.PersistentFlags().StringVar(&GC.NamePrefix, "prefix", "", "name to be prepended to what's passed in (e.g., www.)")
-	rootCmd.PersistentFlags().StringVar(&GC.NameOverride, "override-name", "", "name overrides all passed in names")
-	rootCmd.PersistentFlags().BoolVar(&GC.AlexaFormat, "alexa", false, "is input file from Alexa Top Million download")
-	rootCmd.PersistentFlags().BoolVar(&GC.MetadataFormat, "metadata-passthrough", false, "if input records have the form 'name,METADATA', METADATA will be propagated to the output")
-	rootCmd.PersistentFlags().BoolVar(&GC.IterativeResolution, "iterative", false, "Perform own iteration instead of relying on recursive resolver")
-	rootCmd.PersistentFlags().BoolVar(&GC.LookupAllNameServers, "all-nameservers", false, "Perform the lookup via all the nameservers for the domain.")
-	rootCmd.PersistentFlags().StringVar(&GC.InputFilePath, "input-file", "-", "names to read, defaults to stdin")
-	rootCmd.PersistentFlags().StringVar(&GC.OutputFilePath, "output-file", "-", "where should JSON output be saved, defaults to stdout")
-	rootCmd.PersistentFlags().StringVar(&GC.MetadataFilePath, "metadata-file", "", "where should JSON metadata be saved, defaults to no metadata output. Use '-' for stderr.")
-	rootCmd.PersistentFlags().StringVar(&GC.LogFilePath, "log-file", "", "where should JSON logs be saved, defaults to stderr")
-
-	rootCmd.PersistentFlags().StringVar(&GC.ResultVerbosity, "result-verbosity", "normal", "Sets verbosity of each output record. Options: short, normal, long, trace")
-	rootCmd.PersistentFlags().StringVar(&GC.IncludeInOutput, "include-fields", "", "Comma separated list of fields to additionally output beyond result verbosity. Options: class, protocol, ttl, resolver, flags")
-
-	rootCmd.PersistentFlags().IntVar(&GC.Verbosity, "verbosity", 3, "log verbosity: 1 (lowest)--5 (highest)")
-	rootCmd.PersistentFlags().IntVar(&GC.Retries, "retries", 1, "how many times should zdns retry query if timeout or temporary failure")
-	rootCmd.PersistentFlags().IntVar(&GC.MaxDepth, "max-depth", 10, "how deep should we recurse when performing iterative lookups")
-	rootCmd.PersistentFlags().IntVar(&GC.CacheSize, "cache-size", 10000, "how many items can be stored in internal recursive cache")
-	rootCmd.PersistentFlags().BoolVar(&GC.TCPOnly, "tcp-only", false, "Only perform lookups over TCP")
-	rootCmd.PersistentFlags().BoolVar(&GC.UDPOnly, "udp-only", false, "Only perform lookups over UDP")
-	rootCmd.PersistentFlags().BoolVar(&GC.CheckingDisabled, "checking-disabled", false, "Sends DNS packets with the CD bit set")
-	rootCmd.PersistentFlags().BoolVar(&GC.RecycleSockets, "recycle-sockets", true, "Create long-lived unbound UDP socket for each thread at launch and reuse for all (UDP) queries")
-	rootCmd.PersistentFlags().BoolVar(&GC.NameServerMode, "name-server-mode", false, "Treats input as nameservers to query with a static query rather than queries to send to a static name server")
-	rootCmd.PersistentFlags().BoolVar(&GC.FollowCNAMEs, "follow-cnames", true, "Follow CNAMEs/DNAMEs in the lookup process")
-
-	rootCmd.PersistentFlags().StringVar(&GC.NameServersString, "name-servers", "", "List of DNS servers to use. Can be passed as comma-delimited string or via @/path/to/file. If no port is specified, defaults to 53.")
-	rootCmd.PersistentFlags().StringVar(&GC.LocalAddrString, "local-addr", "", "comma-delimited list of local addresses to use, serve as the source IP for outbound queries")
-	rootCmd.PersistentFlags().StringVar(&GC.LocalIfaceString, "local-interface", "", "local interface to use")
-	rootCmd.PersistentFlags().BoolVar(&GC.IPv4TransportOnly, "4", false, "utilize IPv4 query transport only, incompatible with --6")
-	rootCmd.PersistentFlags().BoolVar(&GC.IPv6TransportOnly, "6", false, "utilize IPv6 query transport only, incompatible with --4")
-	rootCmd.PersistentFlags().BoolVar(&GC.PreferIPv4Iteration, "prefer-ipv4-iteration", false, "Prefer IPv4/A record lookups during iterative resolution. Ignored unless used with both IPv4 and IPv6")
-	rootCmd.PersistentFlags().BoolVar(&GC.PreferIPv6Iteration, "prefer-ipv6-iteration", false, "Prefer IPv6/AAAA record lookups during iterative resolution. Ignored unless used with both IPv4 and IPv6")
-
-	rootCmd.PersistentFlags().StringVar(&GC.ConfigFilePath, "conf-file", zdns.DefaultNameServerConfigFile, "config file for DNS servers")
-	rootCmd.PersistentFlags().IntVar(&GC.Timeout, "timeout", 15, "timeout for resolving a individual name, in seconds")
-	rootCmd.PersistentFlags().IntVar(&GC.IterationTimeout, "iteration-timeout", 4, "timeout for a single iterative step in an iterative query, in seconds. Only applicable with --iterative")
-	rootCmd.PersistentFlags().StringVar(&GC.ClassString, "class", "INET", "DNS class to query. Options: INET, CSNET, CHAOS, HESIOD, NONE, ANY.")
-	rootCmd.PersistentFlags().BoolVar(&GC.UseNanoseconds, "nanoseconds", false, "Use nanosecond resolution timestamps")
-	rootCmd.PersistentFlags().StringVar(&GC.ClientSubnetString, "client-subnet", "", "Client subnet in CIDR format for EDNS0.")
-	rootCmd.PersistentFlags().BoolVar(&GC.Dnssec, "dnssec", false, "Requests DNSSEC records by setting the DNSSEC OK (DO) bit")
-	rootCmd.PersistentFlags().BoolVar(&GC.UseNSID, "nsid", false, "Request NSID.")
-
-	rootCmd.PersistentFlags().Bool("ipv4-lookup", false, "Perform an IPv4 Lookup (requests A records) in modules")
-	rootCmd.PersistentFlags().Bool("ipv6-lookup", false, "Perform an IPv6 Lookup (requests AAAA recoreds) in modules")
-	rootCmd.PersistentFlags().StringVar(&GC.BlacklistFilePath, "blacklist-file", "", "blacklist file for servers to exclude from lookups")
-}
-
-// initConfig reads in config file and ENV variables if set.
-func initConfig() {
-	if cfgFile != "" {
-		// Use config file from the flag.
-		viper.SetConfigFile(cfgFile)
-	} else {
-		// Find home directory.
-		home, err := os.UserHomeDir()
-		cobra.CheckErr(err)
-
-		// Search config in home directory with name ".zdns" (without extension).
-		viper.AddConfigPath(home)
-		viper.SetConfigType("yaml")
-		viper.SetConfigName(".zdns")
+	parser = flags.NewParser(nil, flags.None) // options set in Execute()
+	parser.Command.SubcommandsOptional = true // without this, the user must use a command, makes ./zdns --version impossible, we'll enforce specifying modules ourselves
+	parser.Name = "zdns"
+	parser.ShortDescription = "High-speed, low-drag DNS lookups"
+	parser.LongDescription = `ZDNS is a library and CLI tool for making very fast DNS requests. It's built upon
+https://github.com/zmap/dns (and in turn https://github.com/miekg/dns) for constructing
+and parsing raw DNS packets.
+ZDNS also includes its own recursive resolution and a cache to further optimize performance.`
+	_, err := parser.AddGroup("General Options", "General options for controlling the behavior of zdns", &GC.GeneralOptions)
+	if err != nil {
+		log.Fatalf("could not add ZDNS Options group: %v", err)
 	}
-
-	viper.SetEnvPrefix(util.EnvPrefix)
-	viper.AutomaticEnv()
-
-	// If a config file is found, read it in.
-	if err := viper.ReadInConfig(); err == nil {
-		fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
+	_, err = parser.AddGroup("Query Options", "Options for controlling the fields of the actual DNS queries", &GC.QueryOptions)
+	if err != nil {
+		log.Fatalf("could not add Query Options group: %v", err)
+	}
+	_, err = parser.AddGroup("Network Options", "Options for controlling the network behavior of zdns", &GC.NetworkOptions)
+	if err != nil {
+		log.Fatalf("could not add Network Options group: %v", err)
 	}
-	// Bind the current command's flags to viper
-	util.BindFlags(rootCmd, viper.GetViper(), util.EnvPrefix)
+	_, err = parser.AddGroup("Input/Output Options", "Options for controlling the input and output behavior of zdns", &GC.InputOutputOptions)
+	if err != nil {
+		log.Fatalf("could not add Input/Output Options group: %v", err)
+	}
+
 }
diff --git a/src/cli/modules.go b/src/cli/modules.go
index 1ace9e0..0db1771 100644
--- a/src/cli/modules.go
+++ b/src/cli/modules.go
@@ -16,18 +16,19 @@ package cli
 import (
 	"fmt"
 
-	"github.com/pkg/errors"
-	"github.com/spf13/pflag"
+	log "github.com/sirupsen/logrus"
 
+	"github.com/pkg/errors"
 	"github.com/zmap/dns"
 
 	"github.com/zmap/zdns/src/zdns"
 )
 
 type LookupModule interface {
-	CLIInit(gc *CLIConf, rc *zdns.ResolverConfig, flags *pflag.FlagSet) error
+	CLIInit(gc *CLIConf, rc *zdns.ResolverConfig) error
 	Lookup(resolver *zdns.Resolver, lookupName, nameServer string) (interface{}, zdns.Trace, zdns.Status, error)
 	Help() string
+	Description() string
 }
 
 const (
@@ -111,6 +112,10 @@ func init() {
 
 func RegisterLookupModule(name string, lm LookupModule) {
 	moduleToLookupModule[name] = lm
+	_, err := parser.AddCommand(name, "", lm.Description(), lm)
+	if err != nil {
+		log.Fatalf("could not add command: %v", err)
+	}
 }
 
 type BasicLookupModule struct {
@@ -120,7 +125,7 @@ type BasicLookupModule struct {
 	DNSClass             uint16
 }
 
-func (lm *BasicLookupModule) CLIInit(gc *CLIConf, rc *zdns.ResolverConfig, flags *pflag.FlagSet) error {
+func (lm *BasicLookupModule) CLIInit(gc *CLIConf, rc *zdns.ResolverConfig) error {
 	if gc == nil {
 		return errors.New("CLIConf cannot be nil")
 	}
@@ -136,6 +141,10 @@ func (lm *BasicLookupModule) Help() string {
 	return ""
 }
 
+func (lm *BasicLookupModule) Description() string {
+	return ""
+}
+
 func (lm *BasicLookupModule) Lookup(resolver *zdns.Resolver, lookupName, nameServer string) (interface{}, zdns.Trace, zdns.Status, error) {
 	if lm.LookupAllNameServers {
 		return resolver.LookupAllNameservers(&zdns.Question{Name: lookupName, Type: lm.DNSType, Class: lm.DNSClass}, nameServer)
@@ -154,10 +163,10 @@ func GetLookupModule(name string) (LookupModule, error) {
 	return module, nil
 }
 
-func GetValidLookups() []string {
-	lookups := make([]string, 0, len(moduleToLookupModule))
+func GetValidLookups() map[string]struct{} {
+	lookups := make(map[string]struct{}, len(moduleToLookupModule))
 	for lookup := range moduleToLookupModule {
-		lookups = append(lookups, lookup)
+		lookups[lookup] = struct{}{}
 	}
 	return lookups
 }
diff --git a/src/cli/mxlookup.go b/src/cli/mxlookup.go
deleted file mode 100644
index 7b0d068..0000000
--- a/src/cli/mxlookup.go
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * ZDNS Copyright 2024 Regents of the University of Michigan
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy
- * of the License at http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- * implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-package cli
-
-import (
-	"strings"
-
-	"github.com/spf13/viper"
-
-	"github.com/zmap/zdns/src/internal/util"
-
-	"github.com/spf13/cobra"
-)
-
-// mxlookupCmd represents the mxlookup command
-var mxlookupCmd = &cobra.Command{
-	Use:   "mxlookup",
-	Short: "Run a more exhaustive mxlookup",
-	Long: `mxlookup will additionally do an A lookup for the IP addresses that
-correspond with an exchange record.`,
-	Run: func(cmd *cobra.Command, args []string) {
-		GC.Module = strings.ToUpper("mxlookup")
-		Run(GC, cmd.Flags())
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(mxlookupCmd)
-
-	mxlookupCmd.PersistentFlags().Bool("ipv4-lookup", false, "perform A lookups for each MX server")
-	mxlookupCmd.PersistentFlags().Bool("ipv6-lookup", false, "perform AAAA record lookups for each MX server")
-	mxlookupCmd.PersistentFlags().Int("mx-cache-size", 1000, "number of records to store in MX -> A/AAAA cache")
-
-	util.BindFlags(mxlookupCmd, viper.GetViper(), util.EnvPrefix)
-}
diff --git a/src/cli/nslookup.go b/src/cli/nslookup.go
deleted file mode 100644
index f1feecd..0000000
--- a/src/cli/nslookup.go
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * ZDNS Copyright 2024 Regents of the University of Michigan
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy
- * of the License at http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- * implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-package cli
-
-import (
-	"strings"
-
-	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
-
-	"github.com/zmap/zdns/src/internal/util"
-)
-
-// nslookupCmd represents the nslookup command
-var nslookupCmd = &cobra.Command{
-	Use:   "nslookup",
-	Short: "Run a more exhaustive nslookup",
-	Long:  `nslookup will additionally do an A/AAAA lookup for the IP addresses that correspond with name server records.`,
-	Run: func(cmd *cobra.Command, args []string) {
-		GC.Module = strings.ToUpper("nslookup")
-		Run(GC, cmd.Flags())
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(nslookupCmd)
-
-	nslookupCmd.PersistentFlags().Bool("ipv4-lookup", false, "perform A lookups for each NS server")
-	nslookupCmd.PersistentFlags().Bool("ipv6-lookup", false, "perform AAAA record lookups for each NS server")
-
-	util.BindFlags(nslookupCmd, viper.GetViper(), util.EnvPrefix)
-}
diff --git a/src/cli/worker_manager.go b/src/cli/worker_manager.go
index 51b1bd7..59ecc89 100644
--- a/src/cli/worker_manager.go
+++ b/src/cli/worker_manager.go
@@ -29,7 +29,6 @@ import (
 	"github.com/liip/sheriff"
 	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
-	"github.com/spf13/pflag"
 	"github.com/zmap/dns"
 
 	"github.com/zmap/zdns/src/cli/iohandlers"
@@ -61,7 +60,7 @@ type Metadata struct {
 	ZDNSVersion string         `json:"zdns_version"`
 }
 
-func populateCLIConfig(gc *CLIConf, flags *pflag.FlagSet) *CLIConf {
+func populateCLIConfig(gc *CLIConf) *CLIConf {
 	if gc.LogFilePath != "" && gc.LogFilePath != "-" {
 		f, err := os.OpenFile(gc.LogFilePath, os.O_WRONLY|os.O_CREATE, util.DefaultFilePermissions)
 		if err != nil {
@@ -170,7 +169,7 @@ func populateResolverConfig(gc *CLIConf) *zdns.ResolverConfig {
 	config.Timeout = time.Second * time.Duration(gc.Timeout)
 	config.IterativeTimeout = time.Second * time.Duration(gc.IterationTimeout)
 	config.LookupAllNameServers = gc.LookupAllNameServers
-	config.FollowCNAMEs = gc.FollowCNAMEs
+	config.FollowCNAMEs = !gc.DisableFollowCNAMEs // ZFlags only allows default-false bool flags. We'll invert here.
 
 	if gc.UseNSID {
 		config.EdnsOptions = append(config.EdnsOptions, new(dns.EDNS0_NSID))
@@ -183,7 +182,7 @@ func populateResolverConfig(gc *CLIConf) *zdns.ResolverConfig {
 	config.Retries = gc.Retries
 	config.MaxDepth = gc.MaxDepth
 	config.CheckingDisabledBit = gc.CheckingDisabled
-	config.ShouldRecycleSockets = gc.RecycleSockets
+	config.ShouldRecycleSockets = !gc.DisableRecycleSockets
 	config.DNSSecEnabled = gc.Dnssec
 	config.DNSConfigFilePath = gc.ConfigFilePath
 
@@ -452,8 +451,8 @@ func populateLocalAddresses(gc *CLIConf, config *zdns.ResolverConfig) (*zdns.Res
 	return config, nil
 }
 
-func Run(gc CLIConf, flags *pflag.FlagSet) {
-	gc = *populateCLIConfig(&gc, flags)
+func Run(gc CLIConf) {
+	gc = *populateCLIConfig(&gc)
 	resolverConfig := populateResolverConfig(&gc)
 	// Log any information about the resolver configuration, according to log level
 	resolverConfig.PrintInfo()
@@ -465,7 +464,7 @@ func Run(gc CLIConf, flags *pflag.FlagSet) {
 	if err != nil {
 		log.Fatal("could not get lookup module: ", err)
 	}
-	err = lookupModule.CLIInit(&gc, resolverConfig, flags)
+	err = lookupModule.CLIInit(&gc, resolverConfig)
 	if err != nil {
 		log.Fatalf("could not initialize lookup module (type: %s): %v", gc.Module, err)
 	}
diff --git a/src/internal/util/util.go b/src/internal/util/util.go
index cbafcad..17d80e2 100644
--- a/src/internal/util/util.go
+++ b/src/internal/util/util.go
@@ -20,14 +20,8 @@ import (
 	"net"
 	"regexp"
 	"strconv"
-	"strings"
 
 	"github.com/pkg/errors"
-
-	log "github.com/sirupsen/logrus"
-	"github.com/spf13/cobra"
-	"github.com/spf13/pflag"
-	"github.com/spf13/viper"
 )
 
 const (
@@ -100,32 +94,6 @@ func SplitIPv4AndIPv6Addrs(addrs []string) (ipv4 []string, ipv6 []string, err er
 	return ipv4, ipv6, nil
 }
 
-// Reference: https://github.com/carolynvs/stingoftheviper/blob/main/main.go
-// For how to make cobra/viper sync up, and still use custom struct
-// Bind each cobra flag to its associated viper configuration (config file and environment variable)
-func BindFlags(cmd *cobra.Command, v *viper.Viper, envPrefix string) {
-	cmd.Flags().VisitAll(func(f *pflag.Flag) {
-		// Environment variables can't have dashes in them, so bind them to their equivalent
-		// keys with underscores, e.g. --alexa to ZDNS_ALEXA
-		if strings.Contains(f.Name, "-") {
-			envVarSuffix := strings.ToUpper(strings.ReplaceAll(f.Name, "-", "_"))
-			err := v.BindEnv(f.Name, fmt.Sprintf("%s_%s", envPrefix, envVarSuffix))
-			if err != nil {
-				log.Fatal("failed to bind environment variable to flag: ", err)
-			}
-		}
-
-		// Apply the viper config value to the flag when the flag is not set and viper has a value
-		if !f.Changed && v.IsSet(f.Name) {
-			val := v.Get(f.Name)
-			err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val))
-			if err != nil {
-				log.Fatalf("failed to set flag (%s) value: %v", f.Name, err)
-			}
-		}
-	})
-}
-
 // IsStringValidDomainName checks if the given string is a valid domain name using regex
 func IsStringValidDomainName(domain string) bool {
 	var domainRegex = regexp.MustCompile(`^(?i)[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*\.[a-z]{2,}$`)
diff --git a/src/modules/alookup/a_lookup.go b/src/modules/alookup/a_lookup.go
index 41dbc77..b0ba2db 100644
--- a/src/modules/alookup/a_lookup.go
+++ b/src/modules/alookup/a_lookup.go
@@ -16,15 +16,14 @@ package alookup
 
 import (
 	"github.com/pkg/errors"
-	"github.com/spf13/pflag"
 
 	"github.com/zmap/zdns/src/cli"
 	"github.com/zmap/zdns/src/zdns"
 )
 
 type ALookupModule struct {
-	IPv4Lookup bool
-	IPv6Lookup bool
+	IPv4Lookup bool `long:"ipv4-lookup" description:"perform A lookups for each server"`
+	IPv6Lookup bool `long:"ipv6-lookup" description:"perform AAAA lookups for each server"`
 	baseModule cli.BasicLookupModule
 }
 
@@ -34,17 +33,9 @@ func init() {
 }
 
 // CLIInit initializes the ALookupModule with the given parameters, used to call ALOOKUP from the command line
-func (aMod *ALookupModule) CLIInit(gc *cli.CLIConf, resolverConfig *zdns.ResolverConfig, f *pflag.FlagSet) error {
-	ipv4Lookup, err := f.GetBool("ipv4-lookup")
-	if err != nil {
-		return errors.Wrap(err, "failed to get ipv4-lookup flag")
-	}
-	ipv6Lookup, err := f.GetBool("ipv6-lookup")
-	if err != nil {
-		return errors.Wrap(err, "failed to get ipv6-lookup flag")
-	}
-	aMod.Init(ipv4Lookup, ipv6Lookup)
-	err = aMod.baseModule.CLIInit(gc, resolverConfig, f)
+func (aMod *ALookupModule) CLIInit(gc *cli.CLIConf, resolverConfig *zdns.ResolverConfig) error {
+	aMod.Init(aMod.IPv4Lookup, aMod.IPv6Lookup)
+	err := aMod.baseModule.CLIInit(gc, resolverConfig)
 	if err != nil {
 		return errors.Wrap(err, "failed to initialize base module")
 	}
@@ -66,3 +57,8 @@ func (aMod *ALookupModule) Lookup(r *zdns.Resolver, lookupName, nameServer strin
 func (aMod *ALookupModule) Help() string {
 	return ""
 }
+
+// Description returns the module's description
+func (aMod *ALookupModule) Description() string {
+	return "alookup will get the information that is typically desired, instead of just the information that exists in a single record. Specifically, alookup acts similar to nslookup and will follow CNAME records."
+}
diff --git a/src/modules/axfr/axfr.go b/src/modules/axfr/axfr.go
index 9a666e2..d2f03e5 100644
--- a/src/modules/axfr/axfr.go
+++ b/src/modules/axfr/axfr.go
@@ -25,8 +25,6 @@ import (
 	"github.com/zmap/zdns/src/modules/nslookup"
 
 	log "github.com/sirupsen/logrus"
-	"github.com/spf13/pflag"
-
 	"github.com/zmap/dns"
 
 	"github.com/zmap/zdns/src/zdns"
@@ -35,7 +33,7 @@ import (
 type AxfrLookupModule struct {
 	cli.BasicLookupModule
 	NSModule      nslookup.NSLookupModule
-	BlacklistPath string
+	BlacklistPath string `long:"blacklist-file" description:"path to blacklist file" default:""`
 	Blacklist     *safeblacklist.SafeBlacklist
 	dns.Transfer
 }
@@ -132,35 +130,28 @@ func (axfrMod *AxfrLookupModule) Help() string {
 }
 
 // CLIInit initializes the AxfrLookupModule with the given parameters, used to call AXFR from the command line
-func (axfrMod *AxfrLookupModule) CLIInit(gc *cli.CLIConf, rc *zdns.ResolverConfig, flags *pflag.FlagSet) error {
+func (axfrMod *AxfrLookupModule) CLIInit(gc *cli.CLIConf, rc *zdns.ResolverConfig) error {
 	if gc == nil {
 		return errors.New("CLIConfig is nil")
 	}
 	if rc == nil {
 		return errors.New("ResolverConfig is nil")
 	}
-	if flags == nil {
-		return errors.New("FlagSet is nil")
-	}
 	if gc.IterativeResolution {
 		log.Fatal("AXFR module does not support iterative resolution")
 	}
 	var err error
-	axfrMod.BlacklistPath, err = flags.GetString("blacklist-file")
-	if err != nil {
-		return errors.Wrap(err, "failed to get blacklist-file flag")
-	}
 	if axfrMod.BlacklistPath != "" {
 		axfrMod.Blacklist = safeblacklist.New()
 		if err = axfrMod.Blacklist.ParseFromFile(axfrMod.BlacklistPath); err != nil {
 			return errors.Wrap(err, "failed to parse blacklist")
 		}
 	}
-	err = axfrMod.NSModule.CLIInit(gc, rc, flags)
+	err = axfrMod.NSModule.CLIInit(gc, rc)
 	if err != nil {
 		return errors.Wrap(err, "failed to initialize NSLookupModule as apart of axfrModule")
 	}
-	if err = axfrMod.BasicLookupModule.CLIInit(gc, rc, flags); err != nil {
+	if err = axfrMod.BasicLookupModule.CLIInit(gc, rc); err != nil {
 		return errors.Wrap(err, "failed to initialize basic lookup module")
 	}
 	return nil
diff --git a/src/modules/bindversion/bindversion.go b/src/modules/bindversion/bindversion.go
index 7de5119..f68c0bc 100644
--- a/src/modules/bindversion/bindversion.go
+++ b/src/modules/bindversion/bindversion.go
@@ -15,7 +15,6 @@
 package bindversion
 
 import (
-	"github.com/spf13/pflag"
 	"github.com/zmap/dns"
 
 	"github.com/zmap/zdns/src/cli"
@@ -42,8 +41,8 @@ func init() {
 }
 
 // CLIInit initializes the BindVersion lookup module
-func (bindVersionMod *BindVersionLookupModule) CLIInit(gc *cli.CLIConf, rc *zdns.ResolverConfig, flags *pflag.FlagSet) error {
-	return bindVersionMod.BasicLookupModule.CLIInit(gc, rc, flags)
+func (bindVersionMod *BindVersionLookupModule) CLIInit(gc *cli.CLIConf, rc *zdns.ResolverConfig) error {
+	return bindVersionMod.BasicLookupModule.CLIInit(gc, rc)
 }
 
 func (bindVersionMod *BindVersionLookupModule) Lookup(r *zdns.Resolver, lookupName, nameServer string) (interface{}, zdns.Trace, zdns.Status, error) {
diff --git a/src/modules/dmarc/dmarc.go b/src/modules/dmarc/dmarc.go
index babe976..7d91f50 100644
--- a/src/modules/dmarc/dmarc.go
+++ b/src/modules/dmarc/dmarc.go
@@ -17,7 +17,6 @@ import (
 	"errors"
 	"regexp"
 
-	"github.com/spf13/pflag"
 	"github.com/zmap/dns"
 
 	"github.com/zmap/zdns/src/cli"
@@ -42,11 +41,11 @@ type DmarcLookupModule struct {
 }
 
 // CLIInit initializes the DMARC lookup module
-func (dmarcMod *DmarcLookupModule) CLIInit(gc *cli.CLIConf, rc *zdns.ResolverConfig, flags *pflag.FlagSet) error {
+func (dmarcMod *DmarcLookupModule) CLIInit(gc *cli.CLIConf, rc *zdns.ResolverConfig) error {
 	dmarcMod.re = regexp.MustCompile(dmarcPrefixRegexp)
 	dmarcMod.BasicLookupModule.DNSType = dns.TypeTXT
 	dmarcMod.BasicLookupModule.DNSClass = dns.ClassINET
-	return dmarcMod.BasicLookupModule.CLIInit(gc, rc, flags)
+	return dmarcMod.BasicLookupModule.CLIInit(gc, rc)
 }
 
 func (dmarcMod *DmarcLookupModule) Lookup(r *zdns.Resolver, lookupName, nameServer string) (interface{}, zdns.Trace, zdns.Status, error) {
diff --git a/src/modules/mxlookup/mx_lookup.go b/src/modules/mxlookup/mx_lookup.go
index 7a251df..a116a25 100644
--- a/src/modules/mxlookup/mx_lookup.go
+++ b/src/modules/mxlookup/mx_lookup.go
@@ -19,7 +19,6 @@ import (
 
 	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
-	"github.com/spf13/pflag"
 	"github.com/zmap/dns"
 
 	"github.com/zmap/zdns/src/cli"
@@ -52,52 +51,37 @@ type MXResult struct {
 }
 
 type MXLookupModule struct {
-	IPv4Lookup  bool
-	IPv6Lookup  bool
-	MXCacheSize int
+	IPv4Lookup  bool `long:"ipv4-lookup" description:"perform A lookups for each MX server"`
+	IPv6Lookup  bool `long:"ipv6-lookup" description:"perform AAAA record lookups for each MX server"`
+	MXCacheSize int  `long:"mx-cache-size" default:"1000" description:"number of records to store in MX -> A/AAAA cache"`
 	CacheHash   *cachehash.CacheHash
 	CHmu        sync.Mutex
 	cli.BasicLookupModule
 }
 
 // CLIInit initializes the MXLookupModule with the given parameters, used to call MXLookup from the command line
-func (mxMod *MXLookupModule) CLIInit(gc *cli.CLIConf, rc *zdns.ResolverConfig, f *pflag.FlagSet) error {
-	ipv4Lookup, err := f.GetBool("ipv4-lookup")
-	if err != nil {
-		return errors.Wrap(err, "failed to get ipv4-lookup flag")
-	}
-	ipv6Lookup, err := f.GetBool("ipv6-lookup")
-	if err != nil {
-		return errors.Wrap(err, "failed to get ipv6-lookup flag")
-	}
-	mxCacheSize, err := f.GetInt("mx-cache-size")
-	if err != nil {
-		return errors.Wrap(err, "failed to get mx-cache-size flag")
-	}
-	if !ipv4Lookup && !ipv6Lookup {
+func (mxMod *MXLookupModule) CLIInit(gc *cli.CLIConf, rc *zdns.ResolverConfig) error {
+	if !mxMod.IPv4Lookup && !mxMod.IPv6Lookup {
 		// need to use one of the two
-		ipv4Lookup = true
+		mxMod.IPv4Lookup = true
 	}
-	mxMod.Init(ipv4Lookup, ipv6Lookup, mxCacheSize)
-	if err = mxMod.BasicLookupModule.CLIInit(gc, rc, f); err != nil {
+	mxMod.Init()
+	if err := mxMod.BasicLookupModule.CLIInit(gc, rc); err != nil {
 		return errors.Wrap(err, "failed to initialize BasicLookupModule")
 	}
 	return nil
 }
 
 // Init initializes the MXLookupModule with the given parameters, used to call MXLookup programmatically
-func (mxMod *MXLookupModule) Init(ipv4Lookup, ipv6Lookup bool, mxCacheSize int) {
-	if !ipv4Lookup && !ipv6Lookup {
+func (mxMod *MXLookupModule) Init() {
+	if !mxMod.IPv4Lookup && !mxMod.IPv6Lookup {
 		log.Fatal("At least one of ipv4-lookup or ipv6-lookup must be true")
 	}
-	if mxCacheSize <= 0 {
-		log.Fatal("mxCacheSize must be greater than 0, got ", mxCacheSize)
+	if mxMod.MXCacheSize <= 0 {
+		log.Fatal("mxCacheSize must be greater than 0, got ", mxMod.MXCacheSize)
 	}
-	mxMod.IPv4Lookup = ipv4Lookup || !ipv6Lookup
-	mxMod.IPv6Lookup = ipv6Lookup
-	mxMod.MXCacheSize = mxCacheSize
 	mxMod.CacheHash = new(cachehash.CacheHash)
-	mxMod.CacheHash.Init(mxCacheSize)
+	mxMod.CacheHash.Init(mxMod.MXCacheSize)
 }
 
 func (mxMod *MXLookupModule) lookupIPs(r *zdns.Resolver, name, nameServer string, ipMode zdns.IPVersionMode) (CachedAddresses, zdns.Trace) {
@@ -155,3 +139,7 @@ func (mxMod *MXLookupModule) Lookup(r *zdns.Resolver, lookupName, nameServer str
 func (mxMod *MXLookupModule) Help() string {
 	return ""
 }
+
+func (mxMod *MXLookupModule) Description() string {
+	return "MXLOOKUP will additionally do an A lookup for the IP addresses that correspond with an exchange record."
+}
diff --git a/src/modules/nslookup/ns_lookup.go b/src/modules/nslookup/ns_lookup.go
index 71c2b01..784458d 100644
--- a/src/modules/nslookup/ns_lookup.go
+++ b/src/modules/nslookup/ns_lookup.go
@@ -17,7 +17,6 @@ package nslookup
 import (
 	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
-	"github.com/spf13/pflag"
 
 	"github.com/zmap/zdns/src/cli"
 	"github.com/zmap/zdns/src/zdns"
@@ -30,31 +29,23 @@ func init() {
 
 type NSLookupModule struct {
 	cli.BasicLookupModule
-	IPv4Lookup bool
-	IPv6Lookup bool
+	IPv4Lookup bool `long:"ipv4-lookup" description:"perform A lookups for each NS server"`
+	IPv6Lookup bool `long:"ipv6-lookup" description:"perform AAAA record lookups for each NS server"`
 	// used for mocking
 	testingLookup func(r *zdns.Resolver, lookupName string, nameServer string) (interface{}, zdns.Trace, zdns.Status, error)
 }
 
 // CLIInit initializes the NSLookupModule with the given parameters, used to call NSLookup from the command line
-func (nsMod *NSLookupModule) CLIInit(gc *cli.CLIConf, resolverConf *zdns.ResolverConfig, f *pflag.FlagSet) error {
-	ipv4Lookup, err := f.GetBool("ipv4-lookup")
-	if err != nil {
-		return errors.Wrap(err, "failed to get ipv4-lookup flag")
-	}
-	ipv6Lookup, err := f.GetBool("ipv6-lookup")
-	if err != nil {
-		return errors.Wrap(err, "failed to get ipv6-lookup flag")
-	}
-	if !ipv4Lookup && !ipv6Lookup {
+func (nsMod *NSLookupModule) CLIInit(gc *cli.CLIConf, resolverConf *zdns.ResolverConfig) error {
+	if !nsMod.IPv4Lookup && !nsMod.IPv6Lookup {
 		log.Debug("NSModule: No IP version specified, defaulting to IPv4")
-		ipv4Lookup = true
+		nsMod.IPv4Lookup = true
 	}
-	err = nsMod.BasicLookupModule.CLIInit(gc, resolverConf, f)
+	err := nsMod.BasicLookupModule.CLIInit(gc, resolverConf)
 	if err != nil {
 		return errors.Wrap(err, "failed to initialize basic lookup module")
 	}
-	nsMod.Init(ipv4Lookup, ipv6Lookup)
+	nsMod.Init(nsMod.IPv4Lookup, nsMod.IPv6Lookup)
 	return nil
 }
 
@@ -88,3 +79,7 @@ func (nsMod *NSLookupModule) Help() string {
 func (nsMod *NSLookupModule) WithTestingLookup(f func(r *zdns.Resolver, lookupName string, nameServer string) (interface{}, zdns.Trace, zdns.Status, error)) {
 	nsMod.testingLookup = f
 }
+
+func (nsMod *NSLookupModule) Description() string {
+	return "Run a more exhaustive ns lookup, will additionally do an A/AAAA lookup for the IP addresses that correspond with name server records."
+}
diff --git a/src/modules/spf/spf.go b/src/modules/spf/spf.go
index 64e1707..6d97c33 100644
--- a/src/modules/spf/spf.go
+++ b/src/modules/spf/spf.go
@@ -17,7 +17,6 @@ import (
 	"errors"
 	"regexp"
 
-	"github.com/spf13/pflag"
 	"github.com/zmap/dns"
 
 	"github.com/zmap/zdns/src/cli"
@@ -42,11 +41,11 @@ type SpfLookupModule struct {
 }
 
 // CLIInit initializes the SPF lookup module
-func (spfMod *SpfLookupModule) CLIInit(gc *cli.CLIConf, rc *zdns.ResolverConfig, flags *pflag.FlagSet) error {
+func (spfMod *SpfLookupModule) CLIInit(gc *cli.CLIConf, rc *zdns.ResolverConfig) error {
 	spfMod.re = regexp.MustCompile(spfPrefixRegexp)
 	spfMod.BasicLookupModule.DNSType = dns.TypeTXT
 	spfMod.BasicLookupModule.DNSClass = dns.ClassINET
-	return spfMod.BasicLookupModule.CLIInit(gc, rc, flags)
+	return spfMod.BasicLookupModule.CLIInit(gc, rc)
 }
 
 func (spfMod *SpfLookupModule) Lookup(r *zdns.Resolver, name, resolver string) (interface{}, zdns.Trace, zdns.Status, error) {


Test patch:
diff --git a/src/cli/config_validation_test.go b/src/cli/config_validation_test.go
index 334a159..88b42ea 100644
--- a/src/cli/config_validation_test.go
+++ b/src/cli/config_validation_test.go
@@ -22,25 +22,33 @@ import (
 func TestValidateNetworkingConfig(t *testing.T) {
 	t.Run("LocalAddr and LocalInterface both specified", func(t *testing.T) {
 		gc := &CLIConf{
-			LocalAddrString:   "1.1.1.1",
-			LocalIfaceString:  "eth0",
-			IPv4TransportOnly: true,
+			NetworkOptions: NetworkOptions{
+				LocalAddrString:   "1.1.1.1",
+				LocalIfaceString:  "eth0",
+				IPv4TransportOnly: true,
+			},
 		}
 		err := populateNetworkingConfig(gc)
 		require.NotNil(t, err, "Expected an error but got nil")
 	})
 	t.Run("Using invalid interface", func(t *testing.T) {
 		gc := &CLIConf{
-			LocalIfaceString:  "invalid_interface",
-			IPv4TransportOnly: true,
+			NetworkOptions: NetworkOptions{
+				LocalIfaceString:  "invalid_interface",
+				IPv4TransportOnly: true,
+			},
 		}
 		err := populateNetworkingConfig(gc)
 		require.NotNil(t, err, "Expected an error but got nil")
 	})
 	t.Run("Using nameserver with port", func(t *testing.T) {
 		gc := &CLIConf{
-			NameServersString: "127.0.0.1:53",
-			IPv4TransportOnly: true,
+			NetworkOptions: NetworkOptions{
+				IPv4TransportOnly: true,
+			},
+			GeneralOptions: GeneralOptions{
+				NameServersString: "127.0.0.1:53",
+			},
 		}
 		err := populateNetworkingConfig(gc)
 		require.Nil(t, err, "Expected no error but got %v", err)
diff --git a/src/modules/axfr/axfr_test.go b/src/modules/axfr/axfr_test.go
index 59263b2..f470740 100644
--- a/src/modules/axfr/axfr_test.go
+++ b/src/modules/axfr/axfr_test.go
@@ -19,7 +19,6 @@ import (
 	"reflect"
 	"testing"
 
-	"github.com/spf13/pflag"
 	"github.com/zmap/dns"
 	"gotest.tools/v3/assert"
 
@@ -98,12 +97,8 @@ func InitTest() (*AxfrLookupModule, *zdns.Resolver) {
 	rc.LocalAddrsV4 = []net.IP{net.ParseIP("127.0.0.1")}
 	rc.IPVersionMode = zdns.IPv4Only
 
-	flagSet := new(pflag.FlagSet)
-	flagSet.Bool("ipv4-lookup", false, "Use IPv4")
-	flagSet.Bool("ipv6-lookup", false, "Use IPv6")
-	flagSet.String("blacklist-file", "", "Blacklist")
 	axfrMod := new(AxfrLookupModule)
-	err := axfrMod.CLIInit(cc, rc, flagSet)
+	err := axfrMod.CLIInit(cc, rc)
 	if err != nil {
 		panic("failed to initialize axfr test lookup with error: " + err.Error())
 	}
diff --git a/src/modules/dmarc/dmarc_test.go b/src/modules/dmarc/dmarc_test.go
index b217345..e01b561 100644
--- a/src/modules/dmarc/dmarc_test.go
+++ b/src/modules/dmarc/dmarc_test.go
@@ -66,7 +66,7 @@ func TestDmarcLookup_Valid_1(t *testing.T) {
 			zdns.Answer{Name: "_dmarc.zdns-testing.com", Answer: "v=DMARC1; p=none; rua=mailto:postmaster@censys.io"}},
 	}
 	dmarcMod := DmarcLookupModule{}
-	err := dmarcMod.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{}, nil)
+	err := dmarcMod.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{})
 	assert.NilError(t, err)
 	res, _, status, _ := dmarcMod.Lookup(resolver, "_dmarc.zdns-testing.com", "")
 	assert.Equal(t, queries[0].Class, uint16(dns.ClassINET))
@@ -87,7 +87,7 @@ func TestDmarcLookup_Valid_2(t *testing.T) {
 			zdns.Answer{Name: "_dmarc.zdns-testing.com", Answer: "V=DMARC1; p=none; rua=mailto:postmaster@censys.io"}},
 	}
 	dmarcMod := DmarcLookupModule{}
-	err := dmarcMod.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{}, nil)
+	err := dmarcMod.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{})
 	assert.NilError(t, err)
 	res, _, status, _ := dmarcMod.Lookup(resolver, "_dmarc.zdns-testing.com", "")
 	assert.Equal(t, queries[0].Class, uint16(dns.ClassINET))
@@ -108,7 +108,7 @@ func TestDmarcLookup_Valid_3(t *testing.T) {
 			zdns.Answer{Name: "_dmarc.zdns-testing.com", Answer: "v\t\t\t=\t\t  DMARC1\t\t; p=none; rua=mailto:postmaster@censys.io"}},
 	}
 	dmarcMod := DmarcLookupModule{}
-	err := dmarcMod.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{}, nil)
+	err := dmarcMod.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{})
 	assert.NilError(t, err)
 	res, _, status, _ := dmarcMod.Lookup(resolver, "_dmarc.zdns-testing.com", "")
 	assert.Equal(t, queries[0].Class, uint16(dns.ClassINET))
@@ -129,7 +129,7 @@ func TestDmarcLookup_NotValid_1(t *testing.T) {
 			zdns.Answer{Name: "_dmarc.zdns-testing.com", Answer: "\t\t   v   =DMARC1; p=none; rua=mailto:postmaster@censys.io"}},
 	}
 	dmarcMod := DmarcLookupModule{}
-	err := dmarcMod.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{}, nil)
+	err := dmarcMod.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{})
 	assert.NilError(t, err)
 	res, _, status, _ := dmarcMod.Lookup(resolver, "_dmarc.zdns-testing.com", "")
 	assert.Equal(t, queries[0].Class, uint16(dns.ClassINET))
@@ -150,7 +150,7 @@ func TestDmarcLookup_NotValid_2(t *testing.T) {
 			zdns.Answer{Name: "_dmarc.zdns-testing.com", Answer: "v=DMARc1; p=none; rua=mailto:postmaster@censys.io"}},
 	}
 	dmarcMod := DmarcLookupModule{}
-	err := dmarcMod.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{}, nil)
+	err := dmarcMod.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{})
 	assert.NilError(t, err)
 	res, _, status, _ := dmarcMod.Lookup(resolver, "_dmarc.zdns-testing.com", "")
 	assert.Equal(t, queries[0].Class, uint16(dns.ClassINET))
@@ -171,7 +171,7 @@ func TestDmarcLookup_NotValid_3(t *testing.T) {
 			zdns.Answer{Name: "_dmarc.zdns-testing.com", Answer: "v=DMARc1. p=none; rua=mailto:postmaster@censys.io"}},
 	}
 	dmarcMod := DmarcLookupModule{}
-	err := dmarcMod.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{}, nil)
+	err := dmarcMod.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{})
 	assert.NilError(t, err)
 	res, _, status, _ := dmarcMod.Lookup(resolver, "_dmarc.zdns-testing.com", "")
 	assert.Equal(t, queries[0].Class, uint16(dns.ClassINET))
diff --git a/src/modules/spf/spf_test.go b/src/modules/spf/spf_test.go
index 55c0ed7..1eab9e6 100644
--- a/src/modules/spf/spf_test.go
+++ b/src/modules/spf/spf_test.go
@@ -67,7 +67,7 @@ func TestLookup_DoTxtLookup_Valid_1(t *testing.T) {
 			zdns.Answer{Name: "google.com", Answer: "v=spf1 mx include:_spf.google.com -all"}},
 	}
 	spfModule := SpfLookupModule{}
-	err := spfModule.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{LookupClient: MockLookup{}}, nil)
+	err := spfModule.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{LookupClient: MockLookup{}})
 	assert.NilError(t, err)
 	res, _, status, _ := spfModule.Lookup(resolver, "google.com", "")
 	assert.Equal(t, queries[0].Class, uint16(dns.ClassINET))
@@ -87,7 +87,7 @@ func TestLookup_DoTxtLookup_Valid_2(t *testing.T) {
 			zdns.Answer{Name: "google.com", Answer: "V=SpF1 mx include:_spf.google.com -all"}},
 	}
 	spfModule := SpfLookupModule{}
-	err := spfModule.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{LookupClient: MockLookup{}}, nil)
+	err := spfModule.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{LookupClient: MockLookup{}})
 	assert.NilError(t, err)
 	res, _, status, _ := spfModule.Lookup(resolver, "google.com", "")
 	assert.Equal(t, queries[0].Class, uint16(dns.ClassINET))
@@ -107,7 +107,7 @@ func TestLookup_DoTxtLookup_NotValid_1(t *testing.T) {
 			zdns.Answer{Name: "google.com", Answer: "  V  =  SpF1 mx include:_spf.google.com -all"}},
 	}
 	spfModule := SpfLookupModule{}
-	err := spfModule.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{LookupClient: MockLookup{}}, nil)
+	err := spfModule.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{LookupClient: MockLookup{}})
 	assert.NilError(t, err)
 	res, _, status, _ := spfModule.Lookup(resolver, "google.com", "")
 	assert.Equal(t, queries[0].Class, uint16(dns.ClassINET))
@@ -127,7 +127,7 @@ func TestLookup_DoTxtLookup_NotValid_2(t *testing.T) {
 			zdns.Answer{Name: "google.com", Answer: "some other TXT record but no SPF"}},
 	}
 	spfModule := SpfLookupModule{}
-	err := spfModule.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{LookupClient: MockLookup{}}, nil)
+	err := spfModule.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{LookupClient: MockLookup{}})
 	assert.NilError(t, err)
 	res, _, status, _ := spfModule.Lookup(resolver, "google.com", "")
 	assert.Equal(t, queries[0].Class, uint16(dns.ClassINET))
@@ -142,7 +142,7 @@ func TestLookup_DoTxtLookup_NotValid_2(t *testing.T) {
 func TestLookup_DoTxtLookup_NoTXT(t *testing.T) {
 	resolver := InitTest(t)
 	spfModule := SpfLookupModule{}
-	err := spfModule.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{LookupClient: MockLookup{}}, nil)
+	err := spfModule.CLIInit(&cli.CLIConf{}, &zdns.ResolverConfig{LookupClient: MockLookup{}})
 	assert.NilError(t, err)
 	res, _, status, _ := spfModule.Lookup(resolver, "example.com", "")
 	assert.Equal(t, queries[0].Class, uint16(dns.ClassINET))