// 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) } }