IP-to-ASN data packed into a handful of binaries, for one
:persistent_term entry.
The ranges of each IP version are stored as a step function: every range start maps to the value of the range, a gap after a range maps to a miss, and consecutive starts with the same value are merged, so no range ends are stored. Looking an address up finds the greatest start at or below it:
index- 65,537 big-endianu32, the position of the greatest start at or below each value of the address's top 16 bits, narrowing the search to a slice;starts- the sorted starts,u32for IPv4 andu128for IPv6;values- au32per start:0for a miss, otherwise a 1-based index into the records.
Each distinct {asn, country} has a 10-byte record (asn::32,
country::16, name offset ::32); names and countries live in two blobs,
and a lookup copies them out. Everything is a binary larger than 64 bytes,
so reading the table copies nothing, results never point into it, and
replacing it only leaves a few words behind in the literal area.
The full iptoasn.com dataset (about 580,000 routed ranges) takes about
10 MB. A lookup is a :persistent_term read and about ten binary matches.
Summary
Types
A range as found in the iptoasn.com data: first and last address, ASN,
country and name. ASN 0 marks unrouted space and is skipped.
Types
@type entry() :: %{asn: pos_integer(), country: String.t(), name: String.t()}
@type row() :: {first :: String.t(), last :: String.t(), asn :: non_neg_integer(), country :: String.t(), name :: String.t()}
A range as found in the iptoasn.com data: first and last address, ASN,
country and name. ASN 0 marks unrouted space and is skipped.
@type t() :: %Limen.Signal.Asn.Table{ bytes: non_neg_integer(), countries: binary(), country_offsets: binary(), loaded_at: integer() | nil, names: binary(), ranges: non_neg_integer(), records: binary(), source: term(), v4: version(), v6: version() }
Functions
@spec from_rows(Enumerable.t(row())) :: {:ok, t()} | {:error, String.t()}
Builds a table from rows (see row/0).
Rows are streamed once when each IP version's ranges come sorted by their first address, as in the iptoasn.com data; otherwise they are sorted first. Where ranges overlap, the later start wins from there on, and of two ranges with the same start, the later row wins. An ASN takes the name of its first row.
@spec lookup(t(), :inet.ip_address()) :: entry() | nil
Returns the entry covering ip, or nil.