port ipgrep to golang

This commit is contained in:
Tobias Kessels
2025-02-07 14:06:39 +01:00
parent 6e35b0a4fa
commit 75fdf8cc9b
2 changed files with 376 additions and 0 deletions

BIN
tools/go/goipgrep/ipgrep Executable file

Binary file not shown.

376
tools/go/goipgrep/ipgrep.go Normal file
View 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)
}
}