Limen.Signal.Asn.Table (limen v0.2.1)

Copy Markdown View Source

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-endian u32, 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, u32 for IPv4 and u128 for IPv6;
  • values - a u32 per start: 0 for 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.

t()

Functions

Builds a table from rows (see row/0).

Returns the entry covering ip, or nil.

Types

entry()

@type entry() :: %{asn: pos_integer(), country: String.t(), name: String.t()}

row()

@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.

t()

@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()
}

version()

@type version() :: {index :: binary(), starts :: binary(), values :: binary()}

Functions

from_rows(rows)

@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.

lookup(table, arg)

@spec lookup(t(), :inet.ip_address()) :: entry() | nil

Returns the entry covering ip, or nil.