port ipgrep to golang
This commit is contained in:
BIN
tools/go/goipgrep/ipgrep
Executable file
BIN
tools/go/goipgrep/ipgrep
Executable file
Binary file not shown.
376
tools/go/goipgrep/ipgrep.go
Normal file
376
tools/go/goipgrep/ipgrep.go
Normal file
@@ -0,0 +1,376 @@
|
||||
// ipgrep.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// IPInfo holds the data we want from ipinfo.io
|
||||
type IPInfo struct {
|
||||
IP string `json:"ip"`
|
||||
Hostname string `json:"hostname"`
|
||||
City string `json:"city"`
|
||||
Region string `json:"region"`
|
||||
Country string `json:"country"`
|
||||
Org string `json:"org"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Command-line flags.
|
||||
var (
|
||||
sortFlag bool
|
||||
uniqFlag bool
|
||||
macFlag bool
|
||||
pingable bool
|
||||
resolveFlag bool
|
||||
lookupFlag bool
|
||||
fileName string
|
||||
)
|
||||
flag.BoolVar(&uniqFlag, "u", false, "only show uniq IPs/MACs (implies -s)")
|
||||
flag.BoolVar(&sortFlag, "s", false, "sort output")
|
||||
flag.BoolVar(&macFlag, "m", false, "grep MAC-IDs instead of IPs")
|
||||
flag.BoolVar(&pingable, "p", false, "only show 'pingable' entries (MACs still beta)")
|
||||
flag.BoolVar(&resolveFlag, "r", false, "resolve (uses host for ip and arping for mac)")
|
||||
flag.BoolVar(&lookupFlag, "l", false, "lookup ip info using ipinfo.io and output CSV: ip,country,region,city,org,hostname")
|
||||
flag.StringVar(&fileName, "f", "", "input file")
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s [-u] [-s] [-m] [-p] [-r] [-l] [-f filename] [file...]\n", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
// If pingable is set, force sorting and uniqueness.
|
||||
if pingable || lookupFlag {
|
||||
sortFlag = true
|
||||
uniqFlag = true
|
||||
}
|
||||
|
||||
if lookupFlag && macFlag {
|
||||
fmt.Fprintln(os.Stderr, "Lookup mode (-l) only works for IP addresses, not MAC addresses.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Regular expressions for IPs or MACs.
|
||||
var pattern string
|
||||
if macFlag {
|
||||
// Supports MAC formats: xx:xx:xx:xx:xx:xx or xxxx.xxxx.xxxx
|
||||
pattern = `(([a-fA-F0-9]{2}[:-]){5}[a-fA-F0-9]{2})|([a-fA-F0-9]{4}\.[a-fA-F0-9]{4}\.[a-fA-F0-9]{4})`
|
||||
} else {
|
||||
// Matches valid IPv4 addresses.
|
||||
pattern = `(((25[0-5])|(2[0-4][0-9])|([0-1]?\d?\d))\.){3}((25[0-5])|(2[0-4][0-9])|([0-1]?\d?\d))`
|
||||
}
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error compiling regex: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Read input from -f file, extra args, or stdin.
|
||||
var inputData []byte
|
||||
if fileName != "" {
|
||||
inputData, err = ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading file %s: %v\n", fileName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else if flag.NArg() > 0 {
|
||||
var buf bytes.Buffer
|
||||
for _, fname := range flag.Args() {
|
||||
data, err := ioutil.ReadFile(fname)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading file %s: %v\n", fname, err)
|
||||
continue
|
||||
}
|
||||
buf.Write(data)
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
inputData = buf.Bytes()
|
||||
} else {
|
||||
inputData, err = io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading stdin: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter matches using the regex.
|
||||
matches := re.FindAllString(string(inputData), -1)
|
||||
if matches == nil {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if sortFlag {
|
||||
sort.Strings(matches)
|
||||
}
|
||||
if uniqFlag {
|
||||
matches = unique(matches)
|
||||
}
|
||||
|
||||
if pingable {
|
||||
matches = filterPingable(matches, macFlag)
|
||||
if sortFlag {
|
||||
sort.Strings(matches)
|
||||
}
|
||||
}
|
||||
|
||||
// If lookup flag is set, perform ipinfo.io lookups with caching.
|
||||
if lookupFlag {
|
||||
cache := loadCache()
|
||||
var cacheMu sync.Mutex
|
||||
results := lookupIPInfo(matches, cache, &cacheMu)
|
||||
|
||||
// Sort the results by IP.
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].IP < results[j].IP
|
||||
})
|
||||
|
||||
// Save the updated cache.
|
||||
saveCache(cache)
|
||||
|
||||
// Output CSV using csv.Writer for proper CSV formatting.
|
||||
w := csv.NewWriter(os.Stdout)
|
||||
// Write header.
|
||||
if err := w.Write([]string{"ip", "country", "region", "city", "org", "hostname"}); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error writing CSV header: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
for _, info := range results {
|
||||
record := []string{
|
||||
info.IP,
|
||||
info.Country,
|
||||
info.Region,
|
||||
info.City,
|
||||
info.Org,
|
||||
info.Hostname,
|
||||
}
|
||||
if err := w.Write(record); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error writing CSV record: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
w.Flush()
|
||||
if err := w.Error(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error flushing CSV data: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If resolve flag is set, perform resolution.
|
||||
if resolveFlag {
|
||||
results := resolveEntries(matches, macFlag)
|
||||
for _, r := range results {
|
||||
fmt.Print(r)
|
||||
if !macFlag {
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, just output the matches.
|
||||
for _, m := range matches {
|
||||
fmt.Println(m)
|
||||
}
|
||||
}
|
||||
|
||||
// unique removes duplicate strings from a slice.
|
||||
func unique(input []string) []string {
|
||||
seen := make(map[string]struct{})
|
||||
var result []string
|
||||
for _, s := range input {
|
||||
if _, ok := seen[s]; !ok {
|
||||
seen[s] = struct{}{}
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// filterPingable runs ping (or arping) concurrently on each entry.
|
||||
func filterPingable(entries []string, mac bool) []string {
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
var result []string
|
||||
|
||||
for _, entry := range entries {
|
||||
wg.Add(1)
|
||||
go func(e string) {
|
||||
defer wg.Done()
|
||||
if isPingable(e, mac) {
|
||||
mu.Lock()
|
||||
result = append(result, e)
|
||||
mu.Unlock()
|
||||
}
|
||||
}(entry)
|
||||
}
|
||||
wg.Wait()
|
||||
return result
|
||||
}
|
||||
|
||||
// isPingable tests if an entry is reachable using ping (or arping for MACs).
|
||||
func isPingable(entry string, mac bool) bool {
|
||||
var cmd *exec.Cmd
|
||||
if mac {
|
||||
cmd = exec.Command("arping", "-c", "1", "-w", "5000000", entry)
|
||||
} else {
|
||||
cmd = exec.Command("ping", "-c", "1", "-w", "1", entry)
|
||||
}
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// resolveEntries performs resolution via external commands.
|
||||
func resolveEntries(entries []string, mac bool) []string {
|
||||
var wg sync.WaitGroup
|
||||
results := make([]string, len(entries))
|
||||
|
||||
for i, entry := range entries {
|
||||
wg.Add(1)
|
||||
go func(i int, e string) {
|
||||
defer wg.Done()
|
||||
if mac {
|
||||
cmd := exec.Command("arping", "-q", "-c", "1", "-w", "5000000", e)
|
||||
if err := cmd.Run(); err == nil {
|
||||
cmd2 := exec.Command("arping", "-c", "1", e)
|
||||
out, err := cmd2.CombinedOutput()
|
||||
if err == nil {
|
||||
results[i] = string(out)
|
||||
} else {
|
||||
results[i] = e + "\n"
|
||||
}
|
||||
} else {
|
||||
results[i] = e + "\n"
|
||||
}
|
||||
} else {
|
||||
cmd := exec.Command("host", e)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
// Extract the hostname via regex (similar to grep -Po '(?<=pointer ).*')
|
||||
reHost := regexp.MustCompile(`(?i)(?<=pointer\s)(\S+)`)
|
||||
match := reHost.FindString(string(out))
|
||||
if match != "" {
|
||||
results[i] = fmt.Sprintf("%s %s", e, match)
|
||||
} else {
|
||||
results[i] = e
|
||||
}
|
||||
} else {
|
||||
results[i] = e
|
||||
}
|
||||
}
|
||||
}(i, entry)
|
||||
}
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
|
||||
// lookupIPInfo queries ipinfo.io for each IP concurrently,
|
||||
// checking a local cache before going to the network.
|
||||
// It returns a slice of IPInfo.
|
||||
func lookupIPInfo(entries []string, cache map[string]IPInfo, cacheMu *sync.Mutex) []IPInfo {
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
var results []IPInfo
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
for _, ip := range entries {
|
||||
wg.Add(1)
|
||||
go func(ip string) {
|
||||
defer wg.Done()
|
||||
// Check cache first.
|
||||
cacheMu.Lock()
|
||||
info, found := cache[ip]
|
||||
cacheMu.Unlock()
|
||||
if found {
|
||||
mu.Lock()
|
||||
results = append(results, info)
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Not in cache; perform HTTP lookup.
|
||||
url := fmt.Sprintf("https://ipinfo.io/%s", ip)
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var newInfo IPInfo
|
||||
if err := json.Unmarshal(body, &newInfo); err != nil {
|
||||
return
|
||||
}
|
||||
// Only add valid responses.
|
||||
if newInfo.IP == "" {
|
||||
return
|
||||
}
|
||||
// Update cache.
|
||||
cacheMu.Lock()
|
||||
cache[ip] = newInfo
|
||||
cacheMu.Unlock()
|
||||
mu.Lock()
|
||||
results = append(results, newInfo)
|
||||
mu.Unlock()
|
||||
}(ip)
|
||||
}
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
|
||||
// loadCache reads the cache from ~/.ipgrep.db (if present)
|
||||
// and returns it as a map[string]IPInfo.
|
||||
func loadCache() map[string]IPInfo {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return make(map[string]IPInfo)
|
||||
}
|
||||
cachePath := home + "/.ipgrep.db"
|
||||
data, err := ioutil.ReadFile(cachePath)
|
||||
if err != nil {
|
||||
// File doesn't exist or can't be read, start with an empty cache.
|
||||
return make(map[string]IPInfo)
|
||||
}
|
||||
var cache map[string]IPInfo
|
||||
if err := json.Unmarshal(data, &cache); err != nil {
|
||||
// If unmarshal fails, use an empty cache.
|
||||
return make(map[string]IPInfo)
|
||||
}
|
||||
return cache
|
||||
}
|
||||
|
||||
// saveCache writes the cache map to ~/.ipgrep.db.
|
||||
func saveCache(cache map[string]IPInfo) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cachePath := home + "/.ipgrep.db"
|
||||
data, err := json.MarshalIndent(cache, "", " ")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error marshaling cache: %v\n", err)
|
||||
return
|
||||
}
|
||||
if err := ioutil.WriteFile(cachePath, data, 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error writing cache file: %v\n", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user