Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 86553b5d61 | |||
| 860241572c | |||
| bc203bfb79 | |||
| 14955ffb2b | |||
| c78e1dd1b3 | |||
| ce0d30666b | |||
| d7fc56cb79 | |||
| ea6fe3ddc6 | |||
| 2f4438c213 | |||
| 2669371ef4 | |||
| e4a083b01a | |||
| 5cc5349f4b | |||
| 6eab4a29f4 | |||
| bd6b92f179 | |||
| 0b17ea524d |
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 MiB After Width: | Height: | Size: 2.2 MiB |
149
README.md
149
README.md
@@ -1,6 +1,20 @@
|
||||
# PTR Stream
|
||||
# PTRStream
|
||||
> High-performance distributed PTR record scanner with real-time streaming output
|
||||
|
||||
PTR Stream is a high-performance reverse DNS *(PTR record)* lookup tool written in Go. It efficiently processes the entire IPv4 address space, performing concurrent DNS lookups with support for custom DNS servers, output logging, and real-time progress visualization.
|
||||

|
||||
|
||||
PTRStream is a fast and efficient PTR record scanner designed for distributed scanning operations. It uses a Linear Congruential Generator *(LCG)* for deterministic IP generation, allowing for easy distribution of work across multiple machines while maintaining pseudo-random ordering.
|
||||
|
||||
## Features
|
||||
|
||||
- Memory-efficient IP range processing using [GoLCG](https://github.com/acidvegas/golcg)
|
||||
- Distributed scanning support via sharding
|
||||
- Real-time NDJSON output for streaming to data pipelines
|
||||
- Support for both PTR and CNAME records
|
||||
- Automatic DNS server rotation from public resolvers
|
||||
- Progress tracking with detailed statistics
|
||||
- Colorized terminal output
|
||||
- CAIDA-style error formatting (with -debug flag)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -8,32 +22,123 @@ PTR Stream is a high-performance reverse DNS *(PTR record)* lookup tool written
|
||||
go install github.com/acidvegas/ptrstream@latest
|
||||
```
|
||||
|
||||
Or, build from source:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/acidvegas/ptrstream
|
||||
cd ptrstream
|
||||
go build
|
||||
```
|
||||
## Options
|
||||
| Flag | Type | Default | Description |
|
||||
|----------|----------|---------|--------------------------------------------|
|
||||
| `-c` | `int` | `100` | Concurrency level |
|
||||
| `-debug` | `bool` | `false` | Show unsuccessful lookups |
|
||||
| `-dns` | `string` | | File containing DNS servers |
|
||||
| `-j` | `bool` | `false` | Output NDJSON to stdout (no TUI) |
|
||||
| `-l` | `bool` | `false` | Loop continuously after completion |
|
||||
| `-o` | `string` | | Path to NDJSON output file |
|
||||
| `-r` | `int` | `2` | Number of retries for failed lookups |
|
||||
| `-s` | `int` | `0` | Seed for IP generation *(0 for random)* |
|
||||
| `-shard` | `string` | | Shard specification *(index/total format)* |
|
||||
| `-t` | `int` | `2` | Timeout for DNS queries |
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
ptrstream [options]
|
||||
# Basic usage
|
||||
ptrstream -o output.json
|
||||
|
||||
# Use specific DNS servers
|
||||
ptrstream -dns resolvers.txt -o output.json
|
||||
|
||||
# Increase concurrency
|
||||
ptrstream -c 200 -o output.json
|
||||
|
||||
# Distributed scanning (4 machines)
|
||||
# Machine 1:
|
||||
ptrstream -shard 1/4 -s 12345 -o shard1.json
|
||||
# Machine 2:
|
||||
ptrstream -shard 2/4 -s 12345 -o shard2.json
|
||||
# Machine 3:
|
||||
ptrstream -shard 3/4 -s 12345 -o shard3.json
|
||||
# Machine 4:
|
||||
ptrstream -shard 4/4 -s 12345 -o shard4.json
|
||||
```
|
||||
|
||||
###### Command Line Arguments
|
||||
| Flag | Description | Default | Example |
|
||||
|---------|--------------------------------------|---------|------------------------|
|
||||
| `-c` | Concurrency level | `100` | `-c 200` |
|
||||
| `-t` | Timeout for DNS queries | `2s` | `-t 5s` |
|
||||
| `-r` | Number of retries for failed lookups | `2` | `-r 3` |
|
||||
| `-dns` | File containing DNS servers | | `-dns nameservers.txt` |
|
||||
| `-debug`| Show unsuccessful lookups | `False` | `-debug` |
|
||||
| `-o` | Path to NDJSON output file | | `-o results.json` |
|
||||
| `-s` | Seed for IP generation | Random | `-s 12345` |
|
||||
| `-shard`| Shard specification | | `-shard 1/4` |
|
||||
## Distributed Scanning
|
||||
|
||||
---
|
||||
PTRStream supports distributed scanning through its sharding system. By using the same seed value across multiple instances with different shard specifications, you can distribute the workload across multiple machines while ensuring:
|
||||
|
||||
- No IP address is scanned twice
|
||||
- Even distribution of work
|
||||
- Deterministic results
|
||||
- Pseudo-random scanning patterns
|
||||
|
||||
For example, to split the work across 4 machines:
|
||||
```bash
|
||||
# Each machine uses the same seed but different shard
|
||||
ptrstream -shard 1/4 -s 12345 # Machine 1
|
||||
ptrstream -shard 2/4 -s 12345 # Machine 2
|
||||
ptrstream -shard 3/4 -s 12345 # Machine 3
|
||||
ptrstream -shard 4/4 -s 12345 # Machine 4
|
||||
```
|
||||
|
||||
## Real-time Data Pipeline Integration
|
||||
|
||||
PTRStream outputs NDJSON *(Newline Delimited JSON)* format, making it perfect for real-time data pipeline integration. Each line contains a complete JSON record with:
|
||||
|
||||
- Timestamp
|
||||
- IP Address
|
||||
- DNS Server used
|
||||
- Record Type *(PTR/CNAME)*
|
||||
- PTR Record
|
||||
- CNAME Target *(if applicable)*
|
||||
- TTL Value
|
||||
|
||||
Example using named pipe to Elasticsearch:
|
||||
```bash
|
||||
# Create a named pipe
|
||||
mkfifo /tmp/ptrstream
|
||||
|
||||
# Start Elasticsearch ingestion in background
|
||||
cat /tmp/ptrstream | elasticsearch-bulk-import &
|
||||
|
||||
# Run PTRStream with pipe output
|
||||
ptrstream -o /tmp/ptrstream
|
||||
```
|
||||
|
||||
## CNAME Support
|
||||
|
||||
PTRStream properly handles CNAME records in PTR responses, providing:
|
||||
- Detection of CNAME chains
|
||||
- Original hostname and target tracking
|
||||
- TTL values for both record types
|
||||
- Distinct coloring in terminal output
|
||||
- CNAME statistics tracking
|
||||
|
||||
Example NDJSON output:
|
||||
```json
|
||||
{"timestamp":"2024-01-05T12:34:56Z","ip_addr":"1.2.3.4","dns_server":"8.8.8.8","ptr_record":"example.com","record_type":"PTR","ttl":3600}
|
||||
{"timestamp":"2024-01-05T12:34:57Z","ip_addr":"5.6.7.8","dns_server":"1.1.1.1","ptr_record":"original.com","record_type":"CNAME","target":"target.com","ttl":600}
|
||||
```
|
||||
|
||||
## Debug Mode
|
||||
|
||||
When running with `-debug`, failed lookups are displayed and logged using CAIDA-style error formatting. Each error is represented as a special `.in-addr.arpa` address:
|
||||
|
||||
```
|
||||
2024-01-05 12:34:56 │ 1.2.3.4 │ 8.8.8.8 │ ERR │ │ FAIL.TIMEOUT.in-addr.arpa
|
||||
2024-01-05 12:34:57 │ 5.6.7.8 │ 1.1.1.1 │ ERR │ │ FAIL.SERVER-FAILURE.in-addr.arpa
|
||||
2024-01-05 12:34:58 │ 9.10.11.12 │ 8.8.4.4 │ ERR │ │ FAIL.NON-AUTHORITATIVE.in-addr.arpa
|
||||
```
|
||||
|
||||
Error types include:
|
||||
- `FAIL.TIMEOUT.in-addr.arpa` - DNS query timed out
|
||||
- `FAIL.SERVER-FAILURE.in-addr.arpa` - DNS server returned an error
|
||||
- `FAIL.NON-AUTHORITATIVE.in-addr.arpa` - No authoritative answer
|
||||
- `FAIL.REFUSED.in-addr.arpa` - Query was refused
|
||||
- `FAIL.NO-PTR-RECORD.in-addr.arpa` - No PTR record exists
|
||||
- And more...
|
||||
|
||||
These errors are also included in the NDJSON output when using `-debug` with either `-o` or `-j`:
|
||||
```json
|
||||
{"seen":"2024-01-05T12:34:56Z","ip":"1.2.3.4","nameserver":"8.8.8.8","record":"FAIL.TIMEOUT.in-addr.arpa","record_type":"ERR","ttl":0}
|
||||
```
|
||||
|
||||
___
|
||||
|
||||
###### Mirrors: [acid.vegas](https://git.acid.vegas/ptrstream) • [SuperNETs](https://git.supernets.org/acidvegas/ptrstream) • [GitHub](https://github.com/acidvegas/ptrstream) • [GitLab](https://gitlab.com/acidvegas/ptrstream) • [Codeberg](https://codeberg.org/acidvegas/ptrstream)
|
||||
|
||||
13
go.mod
13
go.mod
@@ -1,9 +1,10 @@
|
||||
module ptrstream
|
||||
module github.com/acidvegas/ptrstream
|
||||
|
||||
go 1.23.3
|
||||
|
||||
require (
|
||||
github.com/acidvegas/golcg v1.0.1
|
||||
github.com/miekg/dns v1.1.62
|
||||
github.com/rivo/tview v0.0.0-20241103174730-c76f7879f592
|
||||
)
|
||||
|
||||
@@ -13,7 +14,11 @@ require (
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/term v0.17.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/net v0.27.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
|
||||
)
|
||||
|
||||
19
go.sum
19
go.sum
@@ -8,6 +8,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/rivo/tview v0.0.0-20241103174730-c76f7879f592 h1:YIJ+B1hePP6AgynC5TcqpO0H9k3SSoZa2BGyL6vDUzM=
|
||||
github.com/rivo/tview v0.0.0-20241103174730-c76f7879f592/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
@@ -19,34 +21,45 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
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.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
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.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
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.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
690
ptrstream.go
690
ptrstream.go
@@ -2,11 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -16,18 +16,24 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/acidvegas/golcg"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
const defaultResolversURL = "https://raw.githubusercontent.com/trickest/resolvers/refs/heads/main/resolvers.txt"
|
||||
|
||||
type Config struct {
|
||||
concurrency int
|
||||
timeout time.Duration
|
||||
retries int
|
||||
dnsServers []string
|
||||
serverIndex int
|
||||
debug bool
|
||||
outputFile *os.File
|
||||
mu sync.Mutex
|
||||
concurrency int
|
||||
timeout time.Duration
|
||||
retries int
|
||||
dnsServers []string
|
||||
serverIndex int
|
||||
debug bool
|
||||
outputFile *os.File
|
||||
mu sync.Mutex
|
||||
lastDNSUpdate time.Time
|
||||
updateMu sync.Mutex
|
||||
loop bool
|
||||
}
|
||||
|
||||
type Stats struct {
|
||||
@@ -35,10 +41,13 @@ type Stats struct {
|
||||
total uint64
|
||||
lastProcessed uint64
|
||||
lastCheckTime time.Time
|
||||
startTime time.Time
|
||||
success uint64
|
||||
failed uint64
|
||||
cnames uint64
|
||||
speedHistory []float64
|
||||
mu sync.Mutex
|
||||
round uint64
|
||||
}
|
||||
|
||||
func (s *Stats) increment() {
|
||||
@@ -53,7 +62,15 @@ func (s *Stats) incrementFailed() {
|
||||
atomic.AddUint64(&s.failed, 1)
|
||||
}
|
||||
|
||||
func (s *Stats) incrementCNAME() {
|
||||
atomic.AddUint64(&s.cnames, 1)
|
||||
}
|
||||
|
||||
func (c *Config) getNextServer() string {
|
||||
if err := c.updateDNSServers(); err != nil {
|
||||
fmt.Printf("Failed to update DNS servers: %v\n", err)
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -66,14 +83,44 @@ func (c *Config) getNextServer() string {
|
||||
return server
|
||||
}
|
||||
|
||||
func loadDNSServers(filename string) ([]string, error) {
|
||||
if filename == "" {
|
||||
return nil, nil
|
||||
func fetchDefaultResolvers() ([]string, error) {
|
||||
resp, err := http.Get(defaultResolversURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch default resolvers: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var resolvers []string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
for scanner.Scan() {
|
||||
resolver := strings.TrimSpace(scanner.Text())
|
||||
if resolver != "" {
|
||||
resolvers = append(resolvers, resolver)
|
||||
}
|
||||
}
|
||||
|
||||
file, err := os.Open(filename)
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error reading default resolvers: %v", err)
|
||||
}
|
||||
|
||||
return resolvers, nil
|
||||
}
|
||||
|
||||
func loadDNSServers(dnsFile string) ([]string, error) {
|
||||
if dnsFile == "" {
|
||||
resolvers, err := fetchDefaultResolvers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(resolvers) == 0 {
|
||||
return nil, fmt.Errorf("no default resolvers found")
|
||||
}
|
||||
return resolvers, nil
|
||||
}
|
||||
|
||||
file, err := os.Open(dnsFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to open DNS servers file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
@@ -81,63 +128,128 @@ func loadDNSServers(filename string) ([]string, error) {
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
server := strings.TrimSpace(scanner.Text())
|
||||
if server != "" && !strings.HasPrefix(server, "#") {
|
||||
if !strings.Contains(server, ":") {
|
||||
server += ":53"
|
||||
}
|
||||
if server != "" {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
}
|
||||
|
||||
if len(servers) == 0 {
|
||||
return nil, fmt.Errorf("no valid DNS servers found in file")
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error reading DNS servers file: %v", err)
|
||||
}
|
||||
|
||||
return servers, scanner.Err()
|
||||
if len(servers) == 0 {
|
||||
return nil, fmt.Errorf("no DNS servers found in file")
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
func lookupWithRetry(ip string, cfg *Config) ([]string, string, error) {
|
||||
server := cfg.getNextServer()
|
||||
if server == "" {
|
||||
return nil, "", fmt.Errorf("no DNS servers available")
|
||||
}
|
||||
type DNSResponse struct {
|
||||
Names []string
|
||||
Server string
|
||||
RecordType string // "PTR" or "CNAME"
|
||||
Target string // For CNAME records, stores the target
|
||||
TTL uint32 // Add TTL field
|
||||
}
|
||||
|
||||
r := &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
d := net.Dialer{
|
||||
Timeout: cfg.timeout,
|
||||
}
|
||||
return d.DialContext(ctx, "udp", server)
|
||||
},
|
||||
func translateRcode(rcode int) string {
|
||||
switch rcode {
|
||||
case dns.RcodeSuccess:
|
||||
return "Success"
|
||||
case dns.RcodeFormatError:
|
||||
return "Format Error"
|
||||
case dns.RcodeServerFailure:
|
||||
return "Server Failure"
|
||||
case dns.RcodeNameError: // NXDOMAIN
|
||||
return "No Such Domain"
|
||||
case dns.RcodeNotImplemented:
|
||||
return "Not Implemented"
|
||||
case dns.RcodeRefused:
|
||||
return "Query Refused"
|
||||
default:
|
||||
return fmt.Sprintf("DNS Error %d", rcode)
|
||||
}
|
||||
}
|
||||
|
||||
func lookupWithRetry(ip string, cfg *Config) (DNSResponse, string, error) {
|
||||
var lastErr error
|
||||
var lastServer string
|
||||
|
||||
for i := 0; i < cfg.retries; i++ {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.timeout)
|
||||
names, err := r.LookupAddr(ctx, ip)
|
||||
cancel()
|
||||
server := cfg.getNextServer()
|
||||
if server == "" {
|
||||
return DNSResponse{}, "", fmt.Errorf("no DNS servers available")
|
||||
}
|
||||
lastServer = server
|
||||
|
||||
if err == nil {
|
||||
return names, server, nil
|
||||
// Create DNS message
|
||||
m := new(dns.Msg)
|
||||
arpa, err := dns.ReverseAddr(ip)
|
||||
if err != nil {
|
||||
return DNSResponse{}, "", err
|
||||
}
|
||||
m.SetQuestion(arpa, dns.TypePTR)
|
||||
m.RecursionDesired = true
|
||||
|
||||
// Create DNS client
|
||||
c := new(dns.Client)
|
||||
c.Timeout = cfg.timeout
|
||||
|
||||
// Make the query
|
||||
r, _, err := c.Exchange(m, server)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
if i < cfg.retries-1 {
|
||||
server = cfg.getNextServer()
|
||||
if server == "" {
|
||||
return nil, "", fmt.Errorf("no more DNS servers available")
|
||||
if r.Rcode != dns.RcodeSuccess {
|
||||
lastErr = fmt.Errorf("%s", translateRcode(r.Rcode))
|
||||
continue
|
||||
}
|
||||
|
||||
// Process the response
|
||||
if len(r.Answer) > 0 {
|
||||
var names []string
|
||||
var ttl uint32
|
||||
var isCNAME bool
|
||||
var target string
|
||||
|
||||
for _, ans := range r.Answer {
|
||||
switch rr := ans.(type) {
|
||||
case *dns.PTR:
|
||||
names = append(names, rr.Ptr)
|
||||
ttl = rr.Hdr.Ttl
|
||||
case *dns.CNAME:
|
||||
isCNAME = true
|
||||
names = append(names, rr.Hdr.Name)
|
||||
target = rr.Target
|
||||
ttl = rr.Hdr.Ttl
|
||||
}
|
||||
}
|
||||
r = &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
d := net.Dialer{
|
||||
Timeout: cfg.timeout,
|
||||
}
|
||||
return d.DialContext(ctx, "udp", server)
|
||||
},
|
||||
|
||||
if len(names) > 0 {
|
||||
if isCNAME {
|
||||
return DNSResponse{
|
||||
Names: names,
|
||||
Server: server,
|
||||
RecordType: "CNAME",
|
||||
Target: strings.TrimSuffix(target, "."),
|
||||
TTL: ttl,
|
||||
}, server, nil
|
||||
}
|
||||
return DNSResponse{
|
||||
Names: names,
|
||||
Server: server,
|
||||
RecordType: "PTR",
|
||||
TTL: ttl,
|
||||
}, server, nil
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = fmt.Errorf("no PTR records found")
|
||||
}
|
||||
return nil, "", fmt.Errorf("lookup failed after %d retries", cfg.retries)
|
||||
|
||||
return DNSResponse{}, lastServer, lastErr
|
||||
}
|
||||
|
||||
func reverse(ss []string) []string {
|
||||
@@ -179,7 +291,7 @@ func colorizeIPInPtr(ptr, ip string) string {
|
||||
|
||||
matches := re.FindAllStringIndex(ptr, -1)
|
||||
if matches == nil {
|
||||
return "[green]" + ptr
|
||||
return "[white]" + ptr
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
@@ -187,7 +299,7 @@ func colorizeIPInPtr(ptr, ip string) string {
|
||||
|
||||
for _, match := range matches {
|
||||
if match[0] > lastEnd {
|
||||
result.WriteString("[green]")
|
||||
result.WriteString("[white]")
|
||||
result.WriteString(ptr[lastEnd:match[0]])
|
||||
}
|
||||
result.WriteString("[aqua]")
|
||||
@@ -196,14 +308,21 @@ func colorizeIPInPtr(ptr, ip string) string {
|
||||
}
|
||||
|
||||
if lastEnd < len(ptr) {
|
||||
result.WriteString("[green]")
|
||||
result.WriteString("[white]")
|
||||
result.WriteString(ptr[lastEnd:])
|
||||
}
|
||||
|
||||
finalResult := result.String()
|
||||
finalResult = strings.ReplaceAll(finalResult, ".in-addr.arpa", ".[blue]in-addr.arpa")
|
||||
finalResult = strings.ReplaceAll(finalResult, ".gov", ".[red]gov")
|
||||
finalResult = strings.ReplaceAll(finalResult, ".mil", ".[red]mil")
|
||||
|
||||
if strings.HasSuffix(finalResult, ".in-addr.arpa") {
|
||||
finalResult = finalResult[:len(finalResult)-13] + ".[blue]in-addr.arpa"
|
||||
}
|
||||
if strings.HasSuffix(finalResult, ".gov") {
|
||||
finalResult = finalResult[:len(finalResult)-4] + ".[red]gov"
|
||||
}
|
||||
if strings.HasSuffix(finalResult, ".mil") {
|
||||
finalResult = finalResult[:len(finalResult)-4] + ".[red]mil"
|
||||
}
|
||||
|
||||
return finalResult
|
||||
}
|
||||
@@ -219,18 +338,21 @@ const maxBufferLines = 1000
|
||||
func worker(jobs <-chan string, wg *sync.WaitGroup, cfg *Config, stats *Stats, textView *tview.TextView, app *tview.Application) {
|
||||
defer wg.Done()
|
||||
for ip := range jobs {
|
||||
var names []string
|
||||
var server string
|
||||
var err error
|
||||
timestamp := time.Now()
|
||||
var response DNSResponse
|
||||
var err error
|
||||
var server string
|
||||
|
||||
if len(cfg.dnsServers) > 0 {
|
||||
names, server, err = lookupWithRetry(ip, cfg)
|
||||
response, server, err = lookupWithRetry(ip, cfg)
|
||||
if idx := strings.Index(server, ":"); idx != -1 {
|
||||
server = server[:idx]
|
||||
}
|
||||
} else {
|
||||
names, err = net.LookupAddr(ip)
|
||||
names, err := net.LookupAddr(ip)
|
||||
if err == nil {
|
||||
response = DNSResponse{Names: names, RecordType: "PTR"}
|
||||
}
|
||||
}
|
||||
|
||||
stats.increment()
|
||||
@@ -238,32 +360,36 @@ func worker(jobs <-chan string, wg *sync.WaitGroup, cfg *Config, stats *Stats, t
|
||||
if err != nil {
|
||||
stats.incrementFailed()
|
||||
if cfg.debug {
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||
errMsg := err.Error()
|
||||
if idx := strings.LastIndex(errMsg, ": "); idx != -1 {
|
||||
errMsg = errMsg[idx+2:]
|
||||
}
|
||||
debugLine := fmt.Sprintf("[gray]%s[-] [purple]%15s[-] [gray]│[-] [red]%s[-]\n",
|
||||
timestamp,
|
||||
errRecord := formatErrorAsHostname(err)
|
||||
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
||||
line := fmt.Sprintf("[gray]%s [gray]│[-] [purple]%15s[-] [gray]│[-] [aqua]%-15s[-] [gray]│[-] [red] ERR [-] [gray]│[-] [gray]%-6s[-] [gray]│[-] [gray]%s[-]\n",
|
||||
timeStr,
|
||||
ip,
|
||||
errMsg)
|
||||
server,
|
||||
"",
|
||||
errRecord)
|
||||
app.QueueUpdateDraw(func() {
|
||||
fmt.Fprint(textView, debugLine)
|
||||
fmt.Fprint(textView, line)
|
||||
textView.ScrollToEnd()
|
||||
})
|
||||
|
||||
// Write to NDJSON if enabled
|
||||
writeNDJSON(cfg, time.Now(), ip, server, errRecord, "ERR", "", 0)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if len(names) == 0 {
|
||||
if len(response.Names) == 0 {
|
||||
stats.incrementFailed()
|
||||
if cfg.debug {
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||
debugLine := fmt.Sprintf("[gray]%s[-] [purple]%15s[-] [gray]│[-] [red]No PTR record[-]\n",
|
||||
timestamp,
|
||||
ip)
|
||||
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
||||
line := fmt.Sprintf("[gray]%s [gray]│[-] [purple]%15s[-] [gray]│[-] [aqua]%-15s[-] [gray]│[-] [red] ERR [-] [gray]│[-] [gray]%-6s[-] [gray]│[-] [red]No PTR record[-]\n",
|
||||
timeStr,
|
||||
ip,
|
||||
server,
|
||||
"")
|
||||
app.QueueUpdateDraw(func() {
|
||||
fmt.Fprint(textView, debugLine)
|
||||
fmt.Fprint(textView, line)
|
||||
textView.ScrollToEnd()
|
||||
})
|
||||
}
|
||||
@@ -273,9 +399,9 @@ func worker(jobs <-chan string, wg *sync.WaitGroup, cfg *Config, stats *Stats, t
|
||||
stats.incrementSuccess()
|
||||
|
||||
ptr := ""
|
||||
for _, name := range names {
|
||||
for _, name := range response.Names {
|
||||
if cleaned := strings.TrimSpace(strings.TrimSuffix(name, ".")); cleaned != "" {
|
||||
ptr = cleaned
|
||||
ptr = strings.ToLower(cleaned)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -284,21 +410,31 @@ func worker(jobs <-chan string, wg *sync.WaitGroup, cfg *Config, stats *Stats, t
|
||||
continue
|
||||
}
|
||||
|
||||
writeNDJSON(cfg, timestamp, ip, server, ptr)
|
||||
writeNDJSON(cfg, timestamp, ip, server, ptr, response.RecordType, response.Target, response.TTL)
|
||||
|
||||
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
||||
recordTypeColor := "[blue] PTR [-]"
|
||||
if response.RecordType == "CNAME" {
|
||||
stats.incrementCNAME()
|
||||
recordTypeColor = "[fuchsia]CNAME[-]"
|
||||
ptr = fmt.Sprintf("%s -> %s", strings.ToLower(ptr), strings.ToLower(response.Target))
|
||||
}
|
||||
|
||||
var line string
|
||||
if len(cfg.dnsServers) > 0 {
|
||||
line = fmt.Sprintf("[gray]%s[-] [purple]%15s[-] [gray]│[-] [yellow]%15s[-] [gray]│[-] %s\n",
|
||||
line = fmt.Sprintf("[gray]%s [gray]│[-] [purple]%15s[-] [gray]│[-] [aqua]%-15s[-] [gray]│[-] %-5s [gray]│[-] %s [gray]│[-] %s\n",
|
||||
timeStr,
|
||||
ip,
|
||||
server,
|
||||
recordTypeColor,
|
||||
colorizeTTL(response.TTL),
|
||||
colorizeIPInPtr(ptr, ip))
|
||||
} else {
|
||||
line = fmt.Sprintf("[gray]%s[-] [purple]%15s[-] [gray]│[-] %s\n",
|
||||
line = fmt.Sprintf("[gray]%s [gray]│[-] [purple]%15s[-] [gray]│[-] %-5s [gray]│[-] %s [gray]│[-] %s\n",
|
||||
timeStr,
|
||||
ip,
|
||||
recordTypeColor,
|
||||
colorizeTTL(response.TTL),
|
||||
colorizeIPInPtr(ptr, ip))
|
||||
}
|
||||
|
||||
@@ -343,6 +479,38 @@ func parseShardArg(shard string) (int, int, error) {
|
||||
return shardNum, totalShards, nil
|
||||
}
|
||||
|
||||
func (c *Config) updateDNSServers() error {
|
||||
c.updateMu.Lock()
|
||||
defer c.updateMu.Unlock()
|
||||
|
||||
if time.Since(c.lastDNSUpdate) < 24*time.Hour {
|
||||
return nil
|
||||
}
|
||||
|
||||
resolvers, err := fetchDefaultResolvers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(resolvers) == 0 {
|
||||
return fmt.Errorf("no resolvers found in update")
|
||||
}
|
||||
|
||||
for i, server := range resolvers {
|
||||
if !strings.Contains(server, ":") {
|
||||
resolvers[i] = server + ":53"
|
||||
}
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.dnsServers = resolvers
|
||||
c.serverIndex = 0
|
||||
c.lastDNSUpdate = time.Now()
|
||||
c.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
concurrency := flag.Int("c", 100, "Concurrency level")
|
||||
timeout := flag.Duration("t", 2*time.Second, "Timeout for DNS queries")
|
||||
@@ -352,6 +520,8 @@ func main() {
|
||||
outputPath := flag.String("o", "", "Path to NDJSON output file")
|
||||
seed := flag.Int64("s", 0, "Seed for IP generation (0 for random)")
|
||||
shard := flag.String("shard", "", "Shard specification (e.g., 1/4 for first shard of 4)")
|
||||
loop := flag.Bool("l", false, "Loop continuously after completion")
|
||||
jsonOutput := flag.Bool("j", false, "Output NDJSON to stdout (no TUI)")
|
||||
flag.Parse()
|
||||
|
||||
shardNum, totalShards, err := parseShardArg(*shard)
|
||||
@@ -364,11 +534,26 @@ func main() {
|
||||
*seed = time.Now().UnixNano()
|
||||
}
|
||||
|
||||
servers, err := loadDNSServers(*dnsFile)
|
||||
if err != nil {
|
||||
fmt.Printf("Error loading DNS servers: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
for i, server := range servers {
|
||||
if !strings.Contains(server, ":") {
|
||||
servers[i] = server + ":53"
|
||||
}
|
||||
}
|
||||
|
||||
cfg := &Config{
|
||||
concurrency: *concurrency,
|
||||
timeout: *timeout,
|
||||
retries: *retries,
|
||||
debug: *debug,
|
||||
concurrency: *concurrency,
|
||||
timeout: *timeout,
|
||||
retries: *retries,
|
||||
debug: *debug,
|
||||
dnsServers: servers,
|
||||
lastDNSUpdate: time.Now(),
|
||||
loop: *loop,
|
||||
}
|
||||
|
||||
if *outputPath != "" {
|
||||
@@ -381,16 +566,6 @@ func main() {
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
if *dnsFile != "" {
|
||||
servers, err := loadDNSServers(*dnsFile)
|
||||
if err != nil {
|
||||
fmt.Printf("Error loading DNS servers: %v\n", err)
|
||||
return
|
||||
}
|
||||
cfg.dnsServers = servers
|
||||
fmt.Printf("Loaded %d DNS servers\n", len(servers))
|
||||
}
|
||||
|
||||
app := tview.NewApplication()
|
||||
|
||||
textView := tview.NewTextView().
|
||||
@@ -409,11 +584,12 @@ func main() {
|
||||
flex := tview.NewFlex().
|
||||
SetDirection(tview.FlexRow).
|
||||
AddItem(textView, 0, 1, false).
|
||||
AddItem(progress, 3, 0, false)
|
||||
AddItem(progress, 4, 0, false)
|
||||
|
||||
stats := &Stats{
|
||||
total: 1 << 32,
|
||||
lastCheckTime: time.Now(),
|
||||
startTime: time.Now(),
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -456,23 +632,25 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
statsText := fmt.Sprintf(" [aqua]Count:[:-] [white]%s [gray]│[-] [aqua]Progress:[:-] [darkgray]%7.2f%%[-] [gray]│[-] [aqua]Rate:[:-] %s [gray]│[-] [aqua]Successful:[:-] [green]✓%s [-][darkgray](%5.1f%%)[-] [gray]│[-] [aqua]Failed:[:-] [red]✗%s [-][darkgray](%5.1f%%)[-] ",
|
||||
// First line: stats
|
||||
statsLine := fmt.Sprintf(" [aqua]Elapsed:[:-] [white]%s [gray]│[-] [aqua]Round:[:-] [white]%d [gray]│[-] [aqua]Count:[:-] [white]%s [gray]│[-] [aqua]Progress:[:-] [darkgray]%.2f%%[-] [gray]│[-] [aqua]Rate:[:-] %s [gray]│[-] [aqua]CNAMEs:[:-] [yellow]%s[-][darkgray] (%.1f%%)[-] [gray]│[-] [aqua]Successful:[:-] [green]✓ %s[-][darkgray] (%.1f%%)[-] [gray]│[-] [aqua]Failed:[:-] [red]✗ %s[-][darkgray] (%.1f%%)[-]\n",
|
||||
formatDuration(time.Since(stats.startTime)),
|
||||
atomic.LoadUint64(&stats.round)+1,
|
||||
formatNumber(processed),
|
||||
percent,
|
||||
colorizeSpeed(avgSpeed),
|
||||
formatNumber(atomic.LoadUint64(&stats.cnames)),
|
||||
float64(atomic.LoadUint64(&stats.cnames))/float64(processed)*100,
|
||||
formatNumber(success),
|
||||
float64(success)/float64(processed)*100,
|
||||
formatNumber(failed),
|
||||
float64(failed)/float64(processed)*100)
|
||||
|
||||
textWidth := visibleLength(statsText)
|
||||
barWidth := width - textWidth - 2 // -2 for the [] characters
|
||||
|
||||
// Ensure barWidth is at least 1
|
||||
// Second line: progress bar
|
||||
barWidth := width - 3 // -3 for the [] and space
|
||||
if barWidth < 1 {
|
||||
// If there's not enough space, just show the stats without the progress bar
|
||||
progress.Clear()
|
||||
fmt.Fprint(progress, statsText)
|
||||
fmt.Fprint(progress, statsLine)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -481,15 +659,13 @@ func main() {
|
||||
filled = barWidth
|
||||
}
|
||||
|
||||
bar := strings.Builder{}
|
||||
bar.WriteString(statsText)
|
||||
bar.WriteString("[")
|
||||
bar.WriteString(strings.Repeat("█", filled))
|
||||
bar.WriteString(strings.Repeat("░", barWidth-filled))
|
||||
bar.WriteString("]")
|
||||
barLine := fmt.Sprintf(" [%s%s]",
|
||||
strings.Repeat("█", filled),
|
||||
strings.Repeat("░", barWidth-filled))
|
||||
|
||||
// Combine both lines with explicit newline
|
||||
progress.Clear()
|
||||
fmt.Fprint(progress, bar.String())
|
||||
fmt.Fprintf(progress, "%s%s", statsLine, barLine)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -497,27 +673,169 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
stream, err := golcg.IPStream("0.0.0.0/0", shardNum, totalShards, int(*seed), nil)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating IP stream: %v\n", err)
|
||||
if *jsonOutput {
|
||||
// JSON-only mode
|
||||
jobs := make(chan string, cfg.concurrency)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Start workers
|
||||
for i := 0; i < cfg.concurrency; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for ip := range jobs {
|
||||
var response DNSResponse
|
||||
var err error
|
||||
var server string
|
||||
|
||||
if len(cfg.dnsServers) > 0 {
|
||||
response, server, err = lookupWithRetry(ip, cfg)
|
||||
if idx := strings.Index(server, ":"); idx != -1 {
|
||||
server = server[:idx]
|
||||
}
|
||||
} else {
|
||||
names, err := net.LookupAddr(ip)
|
||||
if err == nil {
|
||||
response = DNSResponse{Names: names, RecordType: "PTR"}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if cfg.debug {
|
||||
errRecord := formatErrorAsHostname(err)
|
||||
record := struct {
|
||||
Seen string `json:"seen"`
|
||||
IP string `json:"ip"`
|
||||
Nameserver string `json:"nameserver"`
|
||||
Record string `json:"record"`
|
||||
RecordType string `json:"record_type"`
|
||||
TTL uint32 `json:"ttl"`
|
||||
}{
|
||||
Seen: time.Now().Format(time.RFC3339),
|
||||
IP: ip,
|
||||
Nameserver: server,
|
||||
Record: errRecord,
|
||||
RecordType: "ERR",
|
||||
TTL: 0,
|
||||
}
|
||||
if data, err := json.Marshal(record); err == nil {
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if len(response.Names) == 0 {
|
||||
if cfg.debug {
|
||||
record := struct {
|
||||
Seen string `json:"seen"`
|
||||
IP string `json:"ip"`
|
||||
Nameserver string `json:"nameserver"`
|
||||
Record string `json:"record"`
|
||||
RecordType string `json:"record_type"`
|
||||
TTL uint32 `json:"ttl"`
|
||||
}{
|
||||
Seen: time.Now().Format(time.RFC3339),
|
||||
IP: ip,
|
||||
Nameserver: server,
|
||||
Record: "FAIL.NO-PTR-RECORD.in-addr.arpa",
|
||||
RecordType: "ERR",
|
||||
TTL: 0,
|
||||
}
|
||||
if data, err := json.Marshal(record); err == nil {
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
ptr := ""
|
||||
for _, name := range response.Names {
|
||||
if cleaned := strings.TrimSpace(strings.TrimSuffix(name, ".")); cleaned != "" {
|
||||
ptr = cleaned
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ptr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
record := struct {
|
||||
Seen string `json:"seen"`
|
||||
IP string `json:"ip"`
|
||||
Nameserver string `json:"nameserver"`
|
||||
Record string `json:"record"`
|
||||
RecordType string `json:"record_type"`
|
||||
TTL uint32 `json:"ttl"`
|
||||
}{
|
||||
Seen: time.Now().Format(time.RFC3339),
|
||||
IP: ip,
|
||||
Nameserver: server,
|
||||
Record: response.Target,
|
||||
RecordType: response.RecordType,
|
||||
TTL: response.TTL,
|
||||
}
|
||||
|
||||
if response.RecordType != "CNAME" {
|
||||
record.Record = ptr
|
||||
}
|
||||
|
||||
if data, err := json.Marshal(record); err == nil {
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Feed IPs to workers
|
||||
for {
|
||||
stream, err := golcg.IPStream("0.0.0.0/0", shardNum, totalShards, int(*seed), nil)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating IP stream: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
for ip := range stream {
|
||||
jobs <- ip
|
||||
}
|
||||
|
||||
if !cfg.loop {
|
||||
break
|
||||
}
|
||||
}
|
||||
close(jobs)
|
||||
wg.Wait()
|
||||
return
|
||||
}
|
||||
|
||||
jobs := make(chan string, cfg.concurrency)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
stream, err := golcg.IPStream("0.0.0.0/0", shardNum, totalShards, int(*seed), nil)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating IP stream: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
for ip := range stream {
|
||||
jobs <- ip
|
||||
}
|
||||
|
||||
if !cfg.loop {
|
||||
break
|
||||
}
|
||||
}
|
||||
close(jobs)
|
||||
}()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < cfg.concurrency; i++ {
|
||||
wg.Add(1)
|
||||
go worker(jobs, &wg, cfg, stats, textView, app)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for ip := range stream {
|
||||
jobs <- ip
|
||||
}
|
||||
close(jobs)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
app.Stop()
|
||||
@@ -538,14 +856,7 @@ func formatNumber(n uint64) string {
|
||||
}
|
||||
parts = append([]string{s[start:i]}, parts...)
|
||||
}
|
||||
formatted := strings.Join(parts, ",")
|
||||
|
||||
totalWidth := len(fmt.Sprint(1<<32)) + 3
|
||||
for len(formatted) < totalWidth {
|
||||
formatted = " " + formatted
|
||||
}
|
||||
|
||||
return formatted
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func colorizeSpeed(speed float64) string {
|
||||
@@ -568,21 +879,30 @@ func visibleLength(s string) int {
|
||||
return len(noColors)
|
||||
}
|
||||
|
||||
func writeNDJSON(cfg *Config, timestamp time.Time, ip, server, ptr string) {
|
||||
func writeNDJSON(cfg *Config, timestamp time.Time, ip, server, ptr, recordType, target string, ttl uint32) {
|
||||
if cfg.outputFile == nil {
|
||||
return
|
||||
}
|
||||
|
||||
record := struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
IPAddr string `json:"ip_addr"`
|
||||
DNSServer string `json:"dns_server"`
|
||||
PTRRecord string `json:"ptr_record"`
|
||||
Seen string `json:"seen"`
|
||||
IP string `json:"ip"`
|
||||
Nameserver string `json:"nameserver"`
|
||||
Record string `json:"record"`
|
||||
RecordType string `json:"record_type"`
|
||||
TTL uint32 `json:"ttl"`
|
||||
}{
|
||||
Timestamp: timestamp.Format(time.RFC3339),
|
||||
IPAddr: ip,
|
||||
DNSServer: server,
|
||||
PTRRecord: ptr,
|
||||
Seen: timestamp.Format(time.RFC3339),
|
||||
IP: ip,
|
||||
Nameserver: server,
|
||||
Record: target, // For CNAME records, use the target
|
||||
RecordType: recordType,
|
||||
TTL: ttl,
|
||||
}
|
||||
|
||||
// If it's not a CNAME, use the PTR record
|
||||
if recordType != "CNAME" {
|
||||
record.Record = ptr
|
||||
}
|
||||
|
||||
if data, err := json.Marshal(record); err == nil {
|
||||
@@ -592,3 +912,95 @@ func writeNDJSON(cfg *Config, timestamp time.Time, ip, server, ptr string) {
|
||||
cfg.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func formatDuration(d time.Duration) string {
|
||||
d = d.Round(time.Second)
|
||||
|
||||
days := d / (24 * time.Hour)
|
||||
d -= days * 24 * time.Hour
|
||||
|
||||
hours := d / time.Hour
|
||||
d -= hours * time.Hour
|
||||
|
||||
minutes := d / time.Minute
|
||||
d -= minutes * time.Minute
|
||||
|
||||
seconds := d / time.Second
|
||||
|
||||
var result string
|
||||
|
||||
if days > 0 {
|
||||
if hours > 0 && minutes > 0 {
|
||||
result = fmt.Sprintf("%dd %dh %dm", days, hours, minutes)
|
||||
} else if hours > 0 {
|
||||
result = fmt.Sprintf("%dd %dh", days, hours)
|
||||
} else {
|
||||
result = fmt.Sprintf("%dd", days)
|
||||
}
|
||||
} else if hours > 0 {
|
||||
if minutes > 0 {
|
||||
result = fmt.Sprintf("%dh %dm", hours, minutes)
|
||||
} else {
|
||||
result = fmt.Sprintf("%dh", hours)
|
||||
}
|
||||
} else if minutes > 0 {
|
||||
if seconds > 0 {
|
||||
result = fmt.Sprintf("%dm %ds", minutes, seconds)
|
||||
} else {
|
||||
result = fmt.Sprintf("%dm", minutes)
|
||||
}
|
||||
} else {
|
||||
result = fmt.Sprintf("%ds", seconds)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func colorizeTTL(ttl uint32) string {
|
||||
switch {
|
||||
case ttl >= 86400: // 1 day or more
|
||||
return fmt.Sprintf("[#00FF00::b]%-6d[-]", ttl) // Bright green with bold
|
||||
case ttl >= 3600: // 1 hour or more
|
||||
return fmt.Sprintf("[yellow]%-6d[-]", ttl)
|
||||
case ttl >= 300: // 5 minutes or more
|
||||
return fmt.Sprintf("[orange]%-6d[-]", ttl)
|
||||
case ttl >= 60: // 1 minute or more
|
||||
return fmt.Sprintf("[red]%-6d[-]", ttl)
|
||||
default: // Less than 60 seconds
|
||||
return fmt.Sprintf("[gray]%-6d[-]", ttl)
|
||||
}
|
||||
}
|
||||
|
||||
func formatErrorAsHostname(err error) string {
|
||||
errMsg := err.Error()
|
||||
if idx := strings.LastIndex(errMsg, ": "); idx != -1 {
|
||||
errMsg = errMsg[idx+2:]
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(errMsg, "i/o timeout"):
|
||||
return "FAIL.TIMEOUT.in-addr.arpa"
|
||||
case strings.Contains(errMsg, "Server Failure"):
|
||||
return "FAIL.SERVER-FAILURE.in-addr.arpa"
|
||||
case strings.Contains(errMsg, "No Such Domain"):
|
||||
return "FAIL.NON-AUTHORITATIVE.in-addr.arpa"
|
||||
case strings.Contains(errMsg, "refused"):
|
||||
return "FAIL.REFUSED.in-addr.arpa"
|
||||
case strings.Contains(errMsg, "no such host"):
|
||||
return "FAIL.NO-SUCH-HOST.in-addr.arpa"
|
||||
case strings.Contains(errMsg, "connection refused"):
|
||||
return "FAIL.CONNECTION-REFUSED.in-addr.arpa"
|
||||
case strings.Contains(errMsg, "network is unreachable"):
|
||||
return "FAIL.NETWORK-UNREACHABLE.in-addr.arpa"
|
||||
case strings.Contains(errMsg, "no route to host"):
|
||||
return "FAIL.NO-ROUTE.in-addr.arpa"
|
||||
case strings.Contains(errMsg, "Format error"):
|
||||
return "FAIL.FORMAT-ERROR.in-addr.arpa"
|
||||
case strings.Contains(errMsg, "Not Implemented"):
|
||||
return "FAIL.NOT-IMPLEMENTED.in-addr.arpa"
|
||||
case strings.Contains(errMsg, "truncated"):
|
||||
return "FAIL.TRUNCATED.in-addr.arpa"
|
||||
default:
|
||||
return fmt.Sprintf("FAIL.%s.in-addr.arpa", strings.ReplaceAll(strings.ToUpper(errMsg), " ", "-"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user