vpndetection_condition (vpndetection v3.3.2)

View Source

Deciding whether an answer is worth blocking.

A condition is written in the shape of a served answer and keyed by the same names the API uses, so what you write here reads like what you get back. Atom and binary keys both work, which is what lets a condition come out of json:decode/1 on an app's config file as readily as out of source:

  #{is_vpn => true}
  #{is_vpn => true, vpn => #{provider => <<"nordvpn">>}}
  #{is_resproxy => true, resproxy => #{hits => #{gte => 5}}}
  #{vpn => #{confidence => [<<"high">>, <<"medium">>]}}
  [#{is_tor => true}, #{is_resproxy => true}]   % a list is OR

A value may be a scalar (equality, strings without regard to case), a list meaning any-of, a map of gte/gt/lte/lt bounding a number, or a nested condition. A member set to false, null or undefined is ignored entirely - a condition states the positive signals you act on, so there is no way to write "block when this is false", which would otherwise read as blocking everybody.

WRITE STRINGS AS BINARIES. An Erlang string is a list of codepoints, so "nordvpn" and an any-of of those numbers are the same term and no library can tell them apart. A bare string is accepted anyway - io_lib decides, exactly as it does when printing - but that makes an any-of of small printable integers unreachable. Binaries have no such ambiguity, and are what json:decode/1 produces.

Summary

Functions

How many leaf constraints a condition actually carries.

Whether an answer satisfies the condition, and should therefore be blocked.

The top-level members a condition names that this answer did not carry.

Refuse a condition that constrains nothing.

Types

condition/0

-type condition() :: #{atom() | binary() => term()}.

conditions/0

-type conditions() :: condition() | [condition()].

Functions

constraint_count(Value)

-spec constraint_count(term()) -> non_neg_integer().

How many leaf constraints a condition actually carries.

matches(Condition, Result)

-spec matches(conditions(), vpndetection_result:result()) -> boolean().

Whether an answer satisfies the condition, and should therefore be blocked.

missing_members(Condition, Result)

-spec missing_members(conditions(), vpndetection_result:result()) -> [binary()].

The top-level members a condition names that this answer did not carry.

A field your plan does not include is absent rather than false, so a condition naming one can never match and the block would silently never fire. Gating is per top-level member, which is why only the first path segment is checked: a detail object present but empty is a real answer meaning the flag is false, not a plan gap.

A locally answered bogon needs no special case: it is synthesized in the widest shape, so every member is present and nothing reads as missing.

validate(Condition)

-spec validate(conditions() | undefined) -> ok | {error, {constrains_nothing, condition()}}.

Refuse a condition that constrains nothing.

Ignoring false means #{is_vpn => false} and #{} have no terms left to satisfy, so they would match every answer and block all traffic. Nobody writes that on purpose, and failing when the middleware is built beats discovering it in production.