AJ ONeal 8731eaf10b
refactor: decouple gitdataset/ipcohort for multi-file repos
gitshallow: fix double-fetch (pull already fetches), drop redundant -C flags
gitdataset: split into GitDataset[T] (file+atomic) and GitRepo (git+multi-dataset)
  - NewDataset for file-only use, AddDataset to register with a GitRepo
  - one clone/fetch per repo regardless of how many datasets it has
ipcohort: split Cohort into hosts (sorted /32, binary search) + nets (CIDRs, linear)
  - fixes false negatives when broad CIDRs (e.g. /8) precede specific entries
  - fixes Parse() sort-before-copy order bug
  - ReadAll always sorts; unsorted param removed (was dead code)
2026-04-19 22:34:25 -06:00
..

ipcohort

A memory-efficient, fast IP cohort checker for blacklists, whitelists, and ad cohorts.

  • 6 bytes per IP address (5 + 1 for alignment)
  • binary search (not as fast as a trie, but memory is linear)
  • atomic swaps for updates

Example

Check if an IP address belongs to a cohort (such as a blacklist):

func main() {
    ipStr := "92.255.85.72"

    path := "/opt/github.com/bitwire-it/ipblocklist/inbound.txt"
    unsorted := false

	blacklist, err := ipcohort.LoadFile(path, unsorted)
	if err != nil {
		log.Fatalf("Failed to load blacklist: %v", err)
	}

	if blacklist.Contains(ipStr) {
		fmt.Printf("%s is BLOCKED\n", ipStr)
		os.Exit(1)
	}

	fmt.Printf("%s is allowed\n", ipStr)
}

Update the list periodically:

func backgroundUpdate(path string, c *ipcohort.Cohort) {
	ticker := time.NewTicker(1 * time.Hour)
	defer ticker.Stop()

	for range ticker.C {
		needsSort := false
		nextCohort, err := ipcohort.LoadFile(path, needsSort)
		if err != nil {
			log.Printf("reload failed: %v", err)
			continue
		}

		log.Printf("reloaded %d blacklist entries", c.Size())
		c.Swap(nextCohort)
	}
}