From 413c36504e144429b70fb0f5f1eac971fb3901da Mon Sep 17 00:00:00 2001 From: acidvegas Date: Fri, 6 Dec 2024 17:41:03 -0500 Subject: [PATCH] Repository updated to Golang version --- .gitignore | 22 ++ LICENSE | 15 ++ README.md | 46 ++-- arpa-stream.py | 108 --------- go.mod | 19 ++ go.sum | 52 +++++ ptrstream.go | 584 +++++++++++++++++++++++++++++++++++++++++++++++++ ptrstream.py | 154 ------------- 8 files changed, 720 insertions(+), 280 deletions(-) create mode 100644 .gitignore create mode 100644 LICENSE delete mode 100644 arpa-stream.py create mode 100644 go.mod create mode 100644 go.sum create mode 100644 ptrstream.go delete mode 100644 ptrstream.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f75820f --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..13e8db5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2025, acidvegas + +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. \ No newline at end of file diff --git a/README.md b/README.md index a16b5fd..60057b1 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,39 @@ # 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 -- [python](https://www.python.org/) -- [aiodns](https://pypi.org/project/aiodns/) *(pip install aiodns)* +## Installation + +```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 ```bash -python ptrstream.py [options] +ptrstream [options] ``` -| Argument | Description | -| --------------------- | ------------------------------------------------------------ | -| `-c`, `--concurrency` | Control the speed of lookups. *(Default = 100)* | -| `-t`, `--timeout` | Timeout for DNS lookups. *(Default = 5s)* | -| `-r`, `--resolvers` | File containing DNS servers to use for lookups. *(Optional)* | -| `-rt`, `--retries` | Number of times to retry a DNS lookup *(Default = 3)* | -| `-s`, `--seed` | Seed to use for the random number generator. | +###### 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` | -## Preview -![](.screens/preview.gif) +--- -___ - -###### 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) +###### 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) diff --git a/arpa-stream.py b/arpa-stream.py deleted file mode 100644 index 9c5627e..0000000 --- a/arpa-stream.py +++ /dev/null @@ -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} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1cca8f5 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..da598b9 --- /dev/null +++ b/go.sum @@ -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= diff --git a/ptrstream.go b/ptrstream.go new file mode 100644 index 0000000..128c82d --- /dev/null +++ b/ptrstream.go @@ -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() + } +} diff --git a/ptrstream.py b/ptrstream.py deleted file mode 100644 index 1ca309b..0000000 --- a/ptrstream.py +++ /dev/null @@ -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))