Repository updated to Golang version

This commit is contained in:
Dionysus 2024-12-06 17:41:03 -05:00
parent 6b22805e2f
commit 8951577210
Signed by: acidvegas
GPG Key ID: EF4B922DB85DC9DE
8 changed files with 720 additions and 280 deletions

22
.gitignore vendored Normal file
View File

@ -0,0 +1,22 @@
# Binary
ptrstream
ptrstream.exe
# Output files
*.json
*.txt
# Go specific
*.test
*.out
/vendor/
# IDE specific
.idea/
.vscode/
*.swp
*.swo
# OS specific
.DS_Store
Thumbs.db

15
LICENSE Normal file
View File

@ -0,0 +1,15 @@
ISC License
Copyright (c) 2025, acidvegas <acid.vegas@acid.vegas>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@ -1,29 +1,39 @@
# PTR Stream # PTR Stream
The ptrstream repository contains a straightforward yet well-crafted Python script for conducting reverse DNS lookups across the entire IPv4 address range. It systematically generates each IPv4 address in a pseudo-random sequence using a seed, ensuring every possible address is covered. For each IP address, the script performs a PTR *(reverse DNS)* lookup and logs all successful findings. Designed to run continuously, ptrstream is an efficient tool for network monitoring and tracking PTR records globally, making it a practical resource for network enthusiasts and professionals who require a reliable and uncomplicated solution for DNS monitoring. 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.
## Requirements ## Installation
- [python](https://www.python.org/)
- [aiodns](https://pypi.org/project/aiodns/) *(pip install aiodns)* ```bash
go install github.com/acidvegas/ptrstream@latest
```
Or, build from source:
```bash
git clone https://github.com/acidvegas/ptrstream
cd ptrstream
go build
```
## Usage ## Usage
```bash ```bash
python ptrstream.py [options] ptrstream [options]
``` ```
| Argument | Description | ###### Command Line Arguments
| --------------------- | ------------------------------------------------------------ | | Flag | Description | Default | Example |
| `-c`, `--concurrency` | Control the speed of lookups. *(Default = 100)* | |---------|--------------------------------------|---------|------------------------|
| `-t`, `--timeout` | Timeout for DNS lookups. *(Default = 5s)* | | `-c` | Concurrency level | `100` | `-c 200` |
| `-r`, `--resolvers` | File containing DNS servers to use for lookups. *(Optional)* | | `-t` | Timeout for DNS queries | `2s` | `-t 5s` |
| `-rt`, `--retries` | Number of times to retry a DNS lookup *(Default = 3)* | | `-r` | Number of retries for failed lookups | `2` | `-r 3` |
| `-s`, `--seed` | Seed to use for the random number generator. | | `-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` |
## Preview ---
![](.screens/preview.gif)
___ ###### 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)
###### Mirrors
[acid.vegas](https://git.acid.vegas/ptrstream) • [GitHub](https://github.com/acidvegas/ptrstream) • [GitLab](https://gitlab.com/acidvegas/ptrstream) • [SuperNETs](https://git.supernets.org/acidvegas/ptrstream)

View File

@ -1,108 +0,0 @@
#/usr/bin/env python
# arpa stream - developed by acidvegas in python (https://git.acid.vegas/ptrstream)
'''
I have no idea where we are going with this, but I'm sure it'll be fun...
'''
import argparse
import concurrent.futures
import random
try:
import dns.resolver
except ImportError:
raise ImportError('missing required \'dnspython\' library (pip install dnspython)')
class colors:
axfr = '\033[34m'
error = '\033[31m'
success = '\033[32m'
ns_query = '\033[33m'
ns_zone = '\033[36m'
reset = '\033[0m'
def genip() -> str:
'''Generate a random IP address with 1 to 4 octets.'''
num_octets = random.randint(1, 4)
ip_parts = [str(random.randint(0, 255)) for _ in range(num_octets)]
return '.'.join(ip_parts)
def query_ns_records(ip: str) -> list:
'''
Query NS records for a given IP.
:param ip: The IP address to query NS records for.
'''
try:
ns_records = [str(record.target)[:-1] for record in dns.resolver.resolve(f'{ip}.in-addr.arpa', 'NS')]
if ns_records:
print(f'{colors.ns_zone}Queried NS records for {ip}: {ns_records}{colors.reset}')
return ns_records
except Exception as e:
print(f'{colors.error}Error querying NS records for {ip}: {e}{colors.reset}')
return []
def resolve_ns_to_ip(ns_hostname: str) -> list:
'''
Resolve NS hostname to IP.
:param ns_hostname: The NS hostname to resolve.
'''
try:
ns_ips = [ip.address for ip in dns.resolver.resolve(ns_hostname, 'A')]
if ns_ips:
print(f'{colors.ns_query}Resolved NS hostname {ns_hostname} to IPs: {ns_ips}{colors.reset}')
return ns_ips
except Exception as e:
print(f'{colors.error}Error resolving NS {ns_hostname}: {e}{colors.reset}')
return []
def axfr_check(ip: str, ns_ip: str):
'''
Perform AXFR check on a specific nameserver IP.
:param ip: The IP address to perform the AXFR check on.
:param ns_ip: The nameserver IP to perform the AXFR check on.
'''
resolver = dns.resolver.Resolver()
resolver.nameservers = [ns_ip]
try:
if resolver.resolve(f'{ip}.in-addr.arpa', 'AXFR'):
print(f'{colors.success}[SUCCESS]{colors.reset} AXFR on {ns_ip} for {ip}.in-addr.arpa')
return True
except Exception as e:
print(f'{colors.error}[FAIL]{colors.reset} AXFR on {ns_ip} for {ip}.in-addr.arpa - Error: {e}')
return False
def process_ip(ip: str):
'''
Process each IP: Fetch NS records and perform AXFR check.
:param ip: The IP address to process.
'''
for ns_hostname in query_ns_records(ip):
for ns_ip in resolve_ns_to_ip(ns_hostname):
if axfr_check(ip, ns_ip):
return
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='DNS AXFR Check Script')
parser.add_argument('--concurrency', type=int, default=100, help='Number of concurrent workers')
args = parser.parse_args()
with concurrent.futures.ThreadPoolExecutor(max_workers=args.concurrency) as executor:
futures = {executor.submit(process_ip, genip()): ip for ip in range(args.concurrency)}
while True:
done, _ = concurrent.futures.wait(futures, return_when=concurrent.futures.FIRST_COMPLETED)
for future in done:
future.result() # We don't need to store the result as it's already printed
futures[executor.submit(process_ip, genip())] = genip()
futures = {future: ip for future, ip in futures.items() if future not in done}

19
go.mod Normal file
View File

@ -0,0 +1,19 @@
module ptrstream
go 1.23.3
require (
github.com/acidvegas/golcg v1.0.1
github.com/rivo/tview v0.0.0-20241103174730-c76f7879f592
)
require (
github.com/gdamore/encoding v1.0.0 // indirect
github.com/gdamore/tcell/v2 v2.7.1 // indirect
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
)

52
go.sum Normal file
View File

@ -0,0 +1,52 @@
github.com/acidvegas/golcg v1.0.1 h1:u6Ba3NZb7ssW0PIszl5B02OImOwICz4u4ljz+oUHSTU=
github.com/acidvegas/golcg v1.0.1/go.mod h1:fMerl4iGjfD6Rar2xwARQgFRsy+ONuDNS0T85vBqEAE=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.7.1 h1:TiCcmpWHiAU7F0rA2I3S2Y4mmLmO9KHxJ7E1QhYzQbc=
github.com/gdamore/tcell/v2 v2.7.1/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
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/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=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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/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/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/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/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/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/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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

584
ptrstream.go Normal file
View File

@ -0,0 +1,584 @@
package main
import (
"bufio"
"context"
"encoding/json"
"flag"
"fmt"
"net"
"os"
"regexp"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/acidvegas/golcg"
"github.com/rivo/tview"
)
type Config struct {
concurrency int
timeout time.Duration
retries int
dnsServers []string
serverIndex int
debug bool
outputFile *os.File
mu sync.Mutex
}
type Stats struct {
processed uint64
total uint64
lastProcessed uint64
lastCheckTime time.Time
success uint64
failed uint64
speedHistory []float64
mu sync.Mutex
}
func (s *Stats) increment() {
atomic.AddUint64(&s.processed, 1)
}
func (s *Stats) incrementSuccess() {
atomic.AddUint64(&s.success, 1)
}
func (s *Stats) incrementFailed() {
atomic.AddUint64(&s.failed, 1)
}
func (c *Config) getNextServer() string {
c.mu.Lock()
defer c.mu.Unlock()
if len(c.dnsServers) == 0 {
return ""
}
server := c.dnsServers[c.serverIndex]
c.serverIndex = (c.serverIndex + 1) % len(c.dnsServers)
return server
}
func loadDNSServers(filename string) ([]string, error) {
if filename == "" {
return nil, nil
}
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
var servers []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
server := strings.TrimSpace(scanner.Text())
if server != "" && !strings.HasPrefix(server, "#") {
if !strings.Contains(server, ":") {
server += ":53"
}
servers = append(servers, server)
}
}
if len(servers) == 0 {
return nil, fmt.Errorf("no valid DNS servers found in file")
}
return servers, scanner.Err()
}
func lookupWithRetry(ip string, cfg *Config) ([]string, string, error) {
server := cfg.getNextServer()
if server == "" {
return nil, "", fmt.Errorf("no DNS servers available")
}
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)
},
}
for i := 0; i < cfg.retries; i++ {
ctx, cancel := context.WithTimeout(context.Background(), cfg.timeout)
names, err := r.LookupAddr(ctx, ip)
cancel()
if err == nil {
return names, server, nil
}
if i < cfg.retries-1 {
server = cfg.getNextServer()
if server == "" {
return nil, "", fmt.Errorf("no more DNS servers available")
}
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)
},
}
}
}
return nil, "", fmt.Errorf("lookup failed after %d retries", cfg.retries)
}
func reverse(ss []string) []string {
reversed := make([]string, len(ss))
for i, s := range ss {
reversed[len(ss)-1-i] = s
}
return reversed
}
func colorizeIPInPtr(ptr, ip string) string {
specialHosts := []string{"localhost", "undefined.hostname.localhost", "unknown"}
for _, host := range specialHosts {
if strings.EqualFold(ptr, host) {
return "[gray]" + ptr
}
}
octets := strings.Split(ip, ".")
patterns := []string{
strings.ReplaceAll(ip, ".", "\\."),
strings.Join(reverse(strings.Split(ip, ".")), "\\."),
strings.ReplaceAll(ip, ".", "-"),
strings.Join(reverse(strings.Split(ip, ".")), "-"),
}
zeroPadded := make([]string, 4)
for i, octet := range octets {
zeroPadded[i] = fmt.Sprintf("%03d", parseInt(octet))
}
patterns = append(patterns,
strings.Join(zeroPadded, "-"),
strings.Join(reverse(zeroPadded), "-"),
)
pattern := strings.Join(patterns, "|")
re := regexp.MustCompile("(" + pattern + ")")
matches := re.FindAllStringIndex(ptr, -1)
if matches == nil {
return "[green]" + ptr
}
var result strings.Builder
lastEnd := 0
for _, match := range matches {
if match[0] > lastEnd {
result.WriteString("[green]")
result.WriteString(ptr[lastEnd:match[0]])
}
result.WriteString("[aqua]")
result.WriteString(ptr[match[0]:match[1]])
lastEnd = match[1]
}
if lastEnd < len(ptr) {
result.WriteString("[green]")
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")
return finalResult
}
func parseInt(s string) int {
num := 0
fmt.Sscanf(s, "%d", &num)
return num
}
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()
if len(cfg.dnsServers) > 0 {
names, server, err = lookupWithRetry(ip, cfg)
if idx := strings.Index(server, ":"); idx != -1 {
server = server[:idx]
}
} else {
names, err = net.LookupAddr(ip)
}
stats.increment()
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,
ip,
errMsg)
app.QueueUpdateDraw(func() {
fmt.Fprint(textView, debugLine)
textView.ScrollToEnd()
})
}
continue
}
if len(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)
app.QueueUpdateDraw(func() {
fmt.Fprint(textView, debugLine)
textView.ScrollToEnd()
})
}
continue
}
stats.incrementSuccess()
ptr := ""
for _, name := range names {
if cleaned := strings.TrimSpace(strings.TrimSuffix(name, ".")); cleaned != "" {
ptr = cleaned
break
}
}
if ptr == "" {
continue
}
writeNDJSON(cfg, timestamp, ip, server, ptr)
timeStr := time.Now().Format("2006-01-02 15:04:05")
var line string
if len(cfg.dnsServers) > 0 {
line = fmt.Sprintf("[gray]%s[-] [purple]%15s[-] [gray]│[-] [yellow]%15s[-] [gray]│[-] %s\n",
timeStr,
ip,
server,
colorizeIPInPtr(ptr, ip))
} else {
line = fmt.Sprintf("[gray]%s[-] [purple]%15s[-] [gray]│[-] %s\n",
timeStr,
ip,
colorizeIPInPtr(ptr, ip))
}
app.QueueUpdateDraw(func() {
fmt.Fprint(textView, line)
content := textView.GetText(false)
lines := strings.Split(content, "\n")
if len(lines) > maxBufferLines {
newContent := strings.Join(lines[len(lines)-maxBufferLines:], "\n")
textView.Clear()
fmt.Fprint(textView, newContent)
}
textView.ScrollToEnd()
})
}
}
func parseShardArg(shard string) (int, int, error) {
if shard == "" {
return 1, 1, nil
}
parts := strings.Split(shard, "/")
if len(parts) != 2 {
return 0, 0, fmt.Errorf("invalid shard format (expected n/total)")
}
shardNum, err := strconv.Atoi(parts[0])
if err != nil {
return 0, 0, fmt.Errorf("invalid shard number: %v", err)
}
totalShards, err := strconv.Atoi(parts[1])
if err != nil {
return 0, 0, fmt.Errorf("invalid total shards: %v", err)
}
if shardNum < 1 || shardNum > totalShards {
return 0, 0, fmt.Errorf("shard number must be between 1 and total shards")
}
return shardNum, totalShards, nil
}
func main() {
concurrency := flag.Int("c", 100, "Concurrency level")
timeout := flag.Duration("t", 2*time.Second, "Timeout for DNS queries")
retries := flag.Int("r", 2, "Number of retries for failed lookups")
dnsFile := flag.String("dns", "", "File containing DNS servers (one per line)")
debug := flag.Bool("debug", false, "Show unsuccessful lookups")
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)")
flag.Parse()
shardNum, totalShards, err := parseShardArg(*shard)
if err != nil {
fmt.Printf("Error parsing shard argument: %v\n", err)
return
}
if *seed == 0 {
*seed = time.Now().UnixNano()
}
cfg := &Config{
concurrency: *concurrency,
timeout: *timeout,
retries: *retries,
debug: *debug,
}
if *outputPath != "" {
f, err := os.OpenFile(*outputPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Printf("Error opening output file: %v\n", err)
return
}
cfg.outputFile = f
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().
SetDynamicColors(true).
SetScrollable(true).
SetChangedFunc(func() {
app.Draw()
})
textView.SetBorder(true).SetTitle(" PTR Records ")
progress := tview.NewTextView().
SetDynamicColors(true).
SetTextAlign(tview.AlignLeft)
progress.SetBorder(true).SetTitle(" Progress ")
flex := tview.NewFlex().
SetDirection(tview.FlexRow).
AddItem(textView, 0, 1, false).
AddItem(progress, 3, 0, false)
stats := &Stats{
total: 1 << 32,
lastCheckTime: time.Now(),
}
go func() {
const movingAverageWindow = 5
stats.speedHistory = make([]float64, 0, movingAverageWindow)
stats.lastCheckTime = time.Now()
for {
processed := atomic.LoadUint64(&stats.processed)
success := atomic.LoadUint64(&stats.success)
failed := atomic.LoadUint64(&stats.failed)
now := time.Now()
duration := now.Sub(stats.lastCheckTime).Seconds()
if duration >= 1.0 {
stats.mu.Lock()
speed := float64(processed-stats.lastProcessed) / duration
stats.speedHistory = append(stats.speedHistory, speed)
if len(stats.speedHistory) > movingAverageWindow {
stats.speedHistory = stats.speedHistory[1:]
}
var avgSpeed float64
for _, s := range stats.speedHistory {
avgSpeed += s
}
avgSpeed /= float64(len(stats.speedHistory))
stats.lastProcessed = processed
stats.lastCheckTime = now
stats.mu.Unlock()
percent := float64(processed) / float64(stats.total) * 100
app.QueueUpdateDraw(func() {
var width int
_, _, width, _ = progress.GetInnerRect()
if width == 0 {
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%%)[-] ",
formatNumber(processed),
percent,
colorizeSpeed(avgSpeed),
formatNumber(success),
float64(success)/float64(processed)*100,
formatNumber(failed),
float64(failed)/float64(processed)*100)
barWidth := width - visibleLength(statsText) - 2
filled := int(float64(barWidth) * (percent / 100))
if filled > barWidth {
filled = barWidth
}
bar := strings.Builder{}
bar.WriteString(statsText)
bar.WriteString("[")
bar.WriteString(strings.Repeat("█", filled))
bar.WriteString(strings.Repeat("░", barWidth-filled))
bar.WriteString("]")
progress.Clear()
fmt.Fprint(progress, bar.String())
})
}
time.Sleep(100 * time.Millisecond)
}
}()
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
}
jobs := make(chan string, cfg.concurrency)
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()
}()
if err := app.SetRoot(flex, true).EnableMouse(true).Run(); err != nil {
panic(err)
}
}
func formatNumber(n uint64) string {
s := fmt.Sprint(n)
parts := make([]string, 0)
for i := len(s); i > 0; i -= 3 {
start := i - 3
if start < 0 {
start = 0
}
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
}
func colorizeSpeed(speed float64) string {
switch {
case speed >= 500:
return fmt.Sprintf("[green]%5.0f/s[-]", speed)
case speed >= 350:
return fmt.Sprintf("[yellow]%5.0f/s[-]", speed)
case speed >= 200:
return fmt.Sprintf("[orange]%5.0f/s[-]", speed)
case speed >= 100:
return fmt.Sprintf("[red]%5.0f/s[-]", speed)
default:
return fmt.Sprintf("[gray]%5.0f/s[-]", speed)
}
}
func visibleLength(s string) int {
noColors := regexp.MustCompile(`\[[a-zA-Z:-]*\]`).ReplaceAllString(s, "")
return len(noColors)
}
func writeNDJSON(cfg *Config, timestamp time.Time, ip, server, ptr string) {
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"`
}{
Timestamp: timestamp.Format(time.RFC3339),
IPAddr: ip,
DNSServer: server,
PTRRecord: ptr,
}
if data, err := json.Marshal(record); err == nil {
cfg.mu.Lock()
cfg.outputFile.Write(data)
cfg.outputFile.Write([]byte("\n"))
cfg.mu.Unlock()
}
}

View File

@ -1,154 +0,0 @@
#/usr/bin/env python
# ptrstream - developed by acidvegas in python (https://git.acid.vegas/ptrstream)
import argparse
import asyncio
import ipaddress
import os
import random
import time
import urllib.request
try:
import aiodns
except ImportError:
raise ImportError('missing required \'aiodns\' library (pip install aiodns)')
# Do not store these in the results file
bad_hosts = ['localhost','undefined.hostname.localhost','unknown']
# Colors
class colors:
ip = '\033[35m'
ip_match = '\033[96m' # IP address mfound within PTR record
ptr = '\033[93m'
red = '\033[31m' # .gov or .mil indicator
invalid = '\033[90m'
reset = '\033[0m'
grey = '\033[90m'
def get_dns_servers() -> list:
'''Get a list of DNS servers to use for lookups.'''
source = urllib.request.urlopen('https://public-dns.info/nameservers.txt')
results = source.read().decode().split('\n')
return [server for server in results if ':' not in server]
async def rdns(semaphore: asyncio.Semaphore, ip_address: str, resolver: aiodns.DNSResolver):
'''
Perform a reverse DNS lookup on an IP address.
:param semaphore: The semaphore to use for concurrency.
:param ip_address: The IP address to perform a reverse DNS lookup on.
'''
async with semaphore:
reverse_name = ipaddress.ip_address(ip_address).reverse_pointer
try:
answer = await resolver.query(reverse_name, 'PTR')
if answer.name not in bad_hosts and answer.name != ip_address and answer.name != reverse_name:
return ip_address, answer.name, True
else:
return ip_address, answer.name, False
except aiodns.error.DNSError as e:
if e.args[0] == aiodns.error.ARES_ENOTFOUND:
return ip_address, f'{colors.red}No rDNS found{colors.reset}', False
elif e.args[0] == aiodns.error.ARES_ETIMEOUT:
return ip_address, f'{colors.red}DNS query timed out{colors.reset}', False
else:
return ip_address, f'{colors.red}DNS error{colors.grey} ({e.args[1]}){colors.reset}', False
except Exception as e:
return ip_address, f'{colors.red}Unknown error{colors.grey} ({str(e)}){colors.reset}', False
def rig(seed: int) -> str:
'''
Random IP generator.
:param seed: The seed to use for the random number generator.
'''
max_value = 256**4
random.seed(seed)
for _ in range(max_value):
shuffled_index = random.randint(0, max_value - 1)
ip = ipaddress.ip_address(shuffled_index)
yield str(ip)
def fancy_print(ip: str, result: str):
'''
Print the IP address and PTR record in a fancy way.
:param ip: The IP address.
:param result: The PTR record.
'''
if result in ('127.0.0.1', 'localhost','undefined.hostname.localhost','unknown'):
print(f'{colors.ip}{ip.ljust(15)}{colors.reset} {colors.grey}-> {result}{colors.reset}')
else:
if ip in result:
result = result.replace(ip, f'{colors.ip_match}{ip}{colors.ptr}')
elif (daship := ip.replace('.', '-')) in result:
result = result.replace(daship, f'{colors.ip_match}{daship}{colors.ptr}')
elif (revip := '.'.join(ip.split('.')[::-1])) in result:
result = result.replace(revip, f'{colors.ip_match}{revip}{colors.ptr}')
elif (revip := '.'.join(ip.split('.')[::-1]).replace('.','-')) in result:
result = result.replace(revip, f'{colors.ip_match}{revip}{colors.ptr}')
print(f'{colors.ip}{ip.ljust(15)}{colors.reset} {colors.grey}->{colors.reset} {colors.ptr}{result}{colors.reset}')
async def main(args: argparse.Namespace):
'''
Generate random IPs and perform reverse DNS lookups.
:param args: The command-line arguments.
'''
if args.resolvers:
if os.path.exists(args.resolvers):
with open(args.resolvers) as file:
dns_resolvers = [item.strip() for item in file.read().splitlines()]
else:
raise FileNotFoundError(f'could not find file \'{args.resolvers}\'')
else:
dns_resolvers = get_dns_servers()
dns_resolvers = random.shuffle(dns_resolvers)
resolver = aiodns.DNSResolver(nameservers=dns_resolvers, timeout=args.timeout, tries=args.retries, rotate=True)
semaphore = asyncio.Semaphore(args.concurrency)
tasks = []
results_cache = []
seed = random.randint(10**9, 10**10 - 1) if not args.seed else args.seed
ip_generator = rig(seed)
for ip in ip_generator:
if len(tasks) < args.concurrency:
task = asyncio.create_task(rdns(semaphore, ip, resolver))
tasks.append(task)
else:
done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
tasks = list(pending)
for task in done:
ip, result, success = task.result()
if result:
fancy_print(ip, result)
if success:
results_cache.append(f'{ip}:{result}')
if len(results_cache) >= 1000:
stamp = time.strftime('%Y%m%d')
with open(f'ptr_{stamp}_{seed}.txt', 'a') as file:
file.writelines(f"{record}\n" for record in results_cache)
results_cache = []
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Perform asynchronous reverse DNS lookups.')
parser.add_argument('-c', '--concurrency', type=int, default=100, help='Control the speed of lookups.')
parser.add_argument('-t', '--timeout', type=int, default=5, help='Timeout for DNS lookups.')
parser.add_argument('-r', '--resolvers', type=str, help='File containing DNS servers to use for lookups.')
parser.add_argument('-rt', '--retries', type=int, default=3, help='Number of times to retry a DNS lookup.')
parser.add_argument('-s', '--seed', type=int, help='Seed to use for random number generator.')
args = parser.parse_args()
asyncio.run(main(args))