Linx.Netfilter.Expr (Linx v0.1.0)

Copy Markdown View Source

A single netfilter expression — one node in a rule's expression list.

Expressions are the kernel's per-rule building blocks: a payload extraction, a comparison, a lookup, a verdict load. Each carries a :name (the kernel's expression-type string, e.g. "payload", "cmp", "immediate", "lookup") and :data (kind-specific arguments — a keyword list, a map, a verdict, …).

Constructors

  • immediate/1 — load a verdict (or a constant) into the kernel's register 0.
  • new/2 — generic constructor for arbitrary (name, data) pairs. Useful for callers building expressions Linx doesn't yet have a dedicated helper for.

There are also kind-specific helpers (cmp/3, payload/3, meta/2, bitwise/3, ct/2, lookup/2, reject/2, counter/1, NAT helpers). They are extra entry points over the same struct shape, not extra fields.

Inspect

iex> Expr.immediate(Verdict.accept())
#Linx.Netfilter.Expr<immediate accept>

iex> Expr.new(:counter, %{packets: 0, bytes: 0})
#Linx.Netfilter.Expr<counter>

References

Summary

Types

Address-input forms accepted by dnat_to/3 / snat_to/3.

t()

Functions

Bitwise AND-with-mask expression. Used most commonly to mask CIDR-shaped addresses before a cmp comparison.

Comparison expression. Compares the value in sreg against value using op.

Counter expression. Per-rule packet + byte counter; the kernel increments it on every match. Reads back via pull/1..2.

Connection-tracking load expression. Reads key (:state, :mark, etc.) from conntrack state into dreg.

Builds a DNAT statement — destination NAT to addr (and optionally port).

An immediate expression — load a verdict into register 0.

Log expression — emits a per-packet event to the NFLOG subsystem (NFNL_SUBSYS_ULOG) on the named group. Combine with Linx.Netfilter.log_listen/2 for in-BEAM packet observability.

Set-lookup expression. Looks up sreg's value in the named set; emits a verdict on hit (for plain sets) or loads associated data into dreg (for maps and vmaps).

Masquerade expression — SNAT to the outgoing interface's primary address. The kernel resolves the address at packet-traversal time, which makes this the right choice for setups where the outbound IP isn't known at rule-write time (DHCP-assigned WAN, PPP links).

Metadata-load expression. Reads key from the packet's metadata into dreg.

Low-level NAT expression. Most callers want dnat_to/2 or snat_to/2 — those construct the accompanying immediate-load expressions automatically. Use nat/2 directly when you need fine control over register choices, ranges, or flags.

Generic constructor: an expression named name carrying data.

Named-shortcut variant of payload/4 for common header fields.

Payload-extraction expression. Reads len bytes at offset into base-anchored header data and stores in dreg.

Redirect expression — DNAT to the local machine, optionally changing the destination port. The kernel uses the input interface's address as the new destination, which makes this the right choice for transparent proxies / port-shifting on a single host.

Reject expression. Produces an explicit rejection response then drops the packet.

Anonymous-set lookup — tcp dport { 22, 80, 443 } accept.

Builds an SNAT statement — source NAT to addr (and optionally port). Same shape as dnat_to/3.

Returns true if expr is an immediate expression carrying a %Verdict{} — i.e. a terminal expression in a rule.

Types

addr_input()

@type addr_input() ::
  {0..255, 0..255, 0..255, 0..255}
  | {0..65535, 0..65535, 0..65535, 0..65535, 0..65535, 0..65535, 0..65535,
     0..65535}
  | <<_::32>>
  | <<_::128>>
  | String.t()
  | Linx.IP.t()

Address-input forms accepted by dnat_to/3 / snat_to/3.

t()

@type t() :: %Linx.Netfilter.Expr{data: term(), name: atom() | String.t()}

Functions

bitwise(mask, xor, opts \\ [])

@spec bitwise(binary(), binary(), keyword()) :: t()

Bitwise AND-with-mask expression. Used most commonly to mask CIDR-shaped addresses before a cmp comparison.

mask and xor must be the same length; len is that length in bytes (defaults to byte_size(mask)).

cmp(op, value, opts \\ [])

@spec cmp(atom(), binary(), keyword()) :: t()

Comparison expression. Compares the value in sreg against value using op.

op is one of :eq, :neq, :lt, :lte, :gt, :gte. value is a raw binary of the right length (1/2/4/16 bytes depending on what was loaded into sreg).

Most callers don't construct cmp/3 directly — the high-level ~NFT sigil and pipeline helpers compose payload + cmp into the natural "tcp dport 22" shape.

iex> Expr.cmp(:eq, <<22::big-16>>)
#Linx.Netfilter.Expr<cmp>

counter(opts \\ [])

@spec counter(keyword()) :: t()

Counter expression. Per-rule packet + byte counter; the kernel increments it on every match. Reads back via pull/1..2.

ct(key, opts \\ [])

@spec ct(
  atom(),
  keyword()
) :: t()

Connection-tracking load expression. Reads key (:state, :mark, etc.) from conntrack state into dreg.

CT-state matching uses the bitmask integers — wrap the state atom(s) with Linx.Netfilter.Wire.ct_state_bits/1 to produce the comparison value:

[Expr.ct(:state), Expr.cmp(:neq, <<Wire.ct_state_bits(:invalid)::big-32>>)]

dnat_to(addr, port \\ nil, opts \\ [])

@spec dnat_to(addr_input(), pos_integer() | nil, keyword()) :: [t()]

Builds a DNAT statement — destination NAT to addr (and optionally port).

Returns a list of %Expr{}: an immediate-load of the address into a register, an optional immediate-load of the port, then the nat expression itself. Rule.build/2 flattens nested lists, so this composes naturally:

Rule.build([
  Expr.payload(:tcp_dport),
  Expr.cmp(:eq, <<8080::big-16>>),
  Expr.dnat_to({10, 0, 0, 5}, 80)
])

addr may be:

  • an IPv4 4-tuple — {10, 0, 0, 5}.
  • an IPv6 8-tuple — {0xfc00, 0, 0, 0, 0, 0, 0, 1}.
  • a raw binary — 4 bytes for IPv4, 16 for IPv6.
  • a %Linx.IP{} struct.
  • a string — "10.0.0.5" / "fc00::1" (parsed via Linx.IP.parse/1).

port is a non-negative integer (encoded big-endian 16-bit), or nil for address-only NAT.

Options:

  • :flags — passed through to nat/2 (:random, :persistent, …).
  • :reg_addr / :reg_port — override the default register choice (1 for addr, 2 for port).

immediate(v)

@spec immediate(Linx.Netfilter.Verdict.input()) :: t()

An immediate expression — load a verdict into register 0.

The data is the verdict struct itself. Accepts a %Verdict{} or any input form Verdict.new!/1 understands.

This constructor also accepts a constant (a binary or integer) for loading a value into a register prior to a cmp — the kernel uses the same expression for both forms.

iex> Expr.immediate(Verdict.accept())
#Linx.Netfilter.Expr<immediate accept>

iex> Expr.immediate(:drop)
#Linx.Netfilter.Expr<immediate drop>

log(opts \\ [])

@spec log(keyword()) :: t()

Log expression — emits a per-packet event to the NFLOG subsystem (NFNL_SUBSYS_ULOG) on the named group. Combine with Linx.Netfilter.log_listen/2 for in-BEAM packet observability.

Options (all optional):

  • :group — NFLOG group (1..65535). Defaults to 5000 (the Linx convention for "I don't care which group").
  • :prefix — string label that shows up in Event.prefix; useful for tagging rules ("blocked", "audit", …). Max 127 bytes.
  • :snaplen — bytes of packet payload to copy (overrides the consumer's copy_mode/snaplen if set on the rule). 0 = no packet data.
  • :qthreshold — kernel-side queue threshold; packets are batched into multipart netlink messages up to this count.
  • :flags:tcp_seq / :tcp_opt / :ip_opt / :uid / :macdecode (NFLOG*). Most callers want them set at the Log listener side via :flags there instead.

lookup(set_name, opts \\ [])

@spec lookup(
  String.t(),
  keyword()
) :: t()

Set-lookup expression. Looks up sreg's value in the named set; emits a verdict on hit (for plain sets) or loads associated data into dreg (for maps and vmaps).

Options:

  • :sreg — register to look up from (default 1).
  • :dreg — register to store the map's data value in (for maps / vmaps). Omitted for plain sets.
  • :flags[:inv] to invert match (NFT_LOOKUP_F_INV).

masquerade(opts \\ [])

@spec masquerade(keyword()) :: t() | [t()]

Masquerade expression — SNAT to the outgoing interface's primary address. The kernel resolves the address at packet-traversal time, which makes this the right choice for setups where the outbound IP isn't known at rule-write time (DHCP-assigned WAN, PPP links).

Only valid in postrouting chains.

Options:

  • :flags[:random, :fully_random, :persistent].
  • :port_min / :port_max — masquerade with a specific port-range remap (rare; kernel default reuses the original source port).

meta(key, opts \\ [])

@spec meta(
  atom(),
  keyword()
) :: t()

Metadata-load expression. Reads key from the packet's metadata into dreg.

Keys: :len, :protocol, :mark, :iif, :oif, :iifname, :oifname, :nfproto, :l4proto, etc. (see Linx.Netfilter.Wire.meta_key_int/1 for the full list).

nat(type, opts \\ [])

@spec nat(
  atom(),
  keyword()
) :: t()

Low-level NAT expression. Most callers want dnat_to/2 or snat_to/2 — those construct the accompanying immediate-load expressions automatically. Use nat/2 directly when you need fine control over register choices, ranges, or flags.

Required:

  • :type:dnat or :snat.
  • :family:ip | :ip6. Required even in an :inet table; the NAT expression needs to know the address size.

Optional:

  • :reg_addr_min — register holding the min (or only) target address. Defaults to nil (no address remap — only meaningful for SNAT in some configs).
  • :reg_addr_max — register holding the max address of a range. nil for single-address NAT.
  • :reg_proto_min / :reg_proto_max — port range registers.
  • :flags[:random, :fully_random, :persistent, :netmap]. The encoder automatically sets :map_ips when address regs are present and :proto_specified when port regs are present.

new(name, data \\ nil)

@spec new(atom() | String.t(), term()) :: t()

Generic constructor: an expression named name carrying data.

Most callers want one of the kind-specific helpers (immediate/1, cmp/3, payload/3, etc.). Use new/2 when you're constructing an expression Linx doesn't yet have a dedicated helper for.

payload(field, opts \\ [])

@spec payload(
  atom(),
  keyword()
) :: t()

Named-shortcut variant of payload/4 for common header fields.

Supported aliases:

  • :ip_saddr / :ip_daddr — IPv4 source / dest address (network base, offsets 12 / 16, length 4).
  • :ip6_saddr / :ip6_daddr — IPv6 source / dest address (network base, offsets 8 / 24, length 16).
  • :ip_protocol — IPv4 protocol byte (network base, offset 9, length 1).
  • :tcp_sport / :tcp_dport — TCP source / dest port (transport base, offsets 0 / 2, length 2). Same wire form as :udp_sport / :udp_dport.
  • :udp_sport / :udp_dport.
  • :icmp_type / :icmp_code — (transport base, offsets 0 / 1, length 1).

payload(base, offset, len, opts \\ [])

@spec payload(atom(), non_neg_integer(), pos_integer(), keyword()) :: t()

Payload-extraction expression. Reads len bytes at offset into base-anchored header data and stores in dreg.

base is :link / :network / :transport / :inner. Use the named shortcuts (payload(:tcp_dport), etc.) for the common cases.

iex> Expr.payload(:transport, 2, 2)
#Linx.Netfilter.Expr<payload>

iex> Expr.payload(:tcp_dport)  # equivalent to (:transport, 2, 2)
#Linx.Netfilter.Expr<payload>

redirect(opts \\ [])

@spec redirect(keyword()) :: t() | [t()]

Redirect expression — DNAT to the local machine, optionally changing the destination port. The kernel uses the input interface's address as the new destination, which makes this the right choice for transparent proxies / port-shifting on a single host.

Only valid in prerouting / output chains.

Options:

  • :port — redirect to this port (16-bit). Omit to keep the original port.
  • :flags[:random, :fully_random].

reject(type \\ :icmp_unreach, opts \\ [])

@spec reject(
  atom(),
  keyword()
) :: t()

Reject expression. Produces an explicit rejection response then drops the packet.

Types:

  • :icmp_unreach — ICMP destination-unreachable (default for :ip / :ip6 / :inet families). :icmp_code opt sets the ICMP code (defaults to 3 — port-unreachable).
  • :tcp_reset — TCP RST (only valid for TCP packets).
  • :icmpx_unreach — family-agnostic ICMP-unreach (kernel picks ICMP vs ICMPv6 based on packet family).

set_literal(values, key_type, opts \\ [])

@spec set_literal([term()], atom(), keyword()) :: t()

Anonymous-set lookup — tcp dport { 22, 80, 443 } accept.

Returns a sentinel %Expr{name: :__anon_set} that the encoder expands at to_batch/2 time into an auto-generated NFT_SET_F_ANONYMOUS | NFT_SET_F_CONSTANT set plus a regular lookup expression referencing it. The anonymous-set lifecycle is tied to the rule — it lives and dies with it.

values is the same shape as a Linx.Netfilter.Set's elements list. key_type is the same set of atoms Linx.Netfilter.Set.new!/2 accepts.

Rule.build([
  Expr.payload(:tcp_dport),
  Expr.set_literal([22, 80, 443], :inet_service),
  Verdict.accept()
])

Options:

  • :flags — passed to the auto-generated set (:interval, :constant, etc.). :anonymous is always added; :constant is added by default (anonymous sets are constant unless explicitly otherwise).

snat_to(addr, port \\ nil, opts \\ [])

@spec snat_to(addr_input(), pos_integer() | nil, keyword()) :: [t()]

Builds an SNAT statement — source NAT to addr (and optionally port). Same shape as dnat_to/3.

SNAT is meaningful in postrouting chains; the kernel rejects SNAT in prerouting / output.

verdict?(expr)

@spec verdict?(t()) :: boolean()

Returns true if expr is an immediate expression carrying a %Verdict{} — i.e. a terminal expression in a rule.

Also recognises wrapped immediate-verdict in the %{dreg: 0, value: %Verdict{}} shape that the codec uses.