Current section
Files
Jump to
Current section
Files
lib/linx/netfilter/decoder.ex
defmodule Linx.Netfilter.Decoder do
@moduledoc """
Converts kernel-side `%Linx.Netlink.Message{}` payloads back into
`%Linx.Netfilter.*{}` value structs.
The shape mirrors `Linx.Netfilter.Encoder` — one decode function
per entity. `from_msgs/3` groups a stream of decoded entities
into a `%Ruleset{}`.
## Wire format quirks
Same as `Linx.Netfilter.Encoder`: nftables NLA_U32 / NLA_U64 are
**big-endian**, attribute IDs are namespaced.
"""
import Linx.Netfilter.Wire
alias Linx.Netfilter.{Chain, Event, Expr, Rule, Ruleset, Set, Table, Verdict, Wire}
alias Linx.Netfilter.Map, as: NMap
alias Linx.Netlink.{Attr, Message}
alias Linx.Netlink.Nfnl.Codec
# ===========================================================
# Tables
# ===========================================================
@doc """
Decodes a `NEWTABLE` message body into a `%Linx.Netfilter.Table{}`.
`body` is `%Message{payload: body}`'s payload — `nfgenmsg` header
followed by NLAs.
"""
@spec table(binary()) :: Table.t()
def table(body) when is_binary(body) do
{family_int, _ver, _res_id, attrs_bin} = Codec.decode_nfgenmsg(body)
attrs = Attr.decode(attrs_bin)
family = Wire.family_atom(family_int)
name = get_string(attrs, nfta_table_name())
flags_int = get_u32_be(attrs, nfta_table_flags(), 0)
use_count = get_u32_be(attrs, nfta_table_use(), nil)
handle = get_u64_be(attrs, nfta_table_handle(), nil)
userdata = get_binary(attrs, nfta_table_userdata())
%Table{
family: family,
name: name,
flags: Wire.table_flags_atoms(flags_int),
use_count: use_count,
handle: handle,
# The chain/set/object/flowtable decoders attach these by
# name later when the full ruleset is assembled.
chains: %{},
sets: %{},
maps: %{},
objects: %{},
flowtables: %{}
}
|> maybe_attach_userdata(userdata)
end
# ===========================================================
# Chains
# ===========================================================
@doc """
Decodes a `NEWCHAIN` message body into a `%Linx.Netfilter.Chain{}`.
The chain's `:family` comes from the nfgenmsg header in the same
message; it isn't stored on the Chain struct (it lives on the
enclosing Table) but is needed here to map the wire hook number
back to the family-specific hook atom.
Returns `{family, chain}` so the caller (typically `from_msgs/3`)
can attach the chain to the right table.
"""
@spec chain(binary()) :: {Table.family(), Chain.t()}
def chain(body) when is_binary(body) do
{family_int, _ver, _res_id, attrs_bin} = Codec.decode_nfgenmsg(body)
attrs = Attr.decode(attrs_bin)
family = Wire.family_atom(family_int)
name = get_string(attrs, nfta_chain_name())
table = get_string(attrs, nfta_chain_table())
type_str = get_string(attrs, nfta_chain_type())
type = if type_str, do: Wire.chain_type_atom(type_str), else: nil
policy_int = get_u32_be(attrs, nfta_chain_policy(), nil)
policy = if policy_int, do: Wire.policy_atom(policy_int), else: nil
flags_int = get_u32_be(attrs, nfta_chain_flags(), 0)
handle = get_u64_be(attrs, nfta_chain_handle(), nil)
{hook, priority, device} = decode_hook_attrs(attrs, family)
chain =
%Chain{
name: name,
table: table,
type: type,
hook: hook,
priority: priority,
policy: policy,
device: device,
flags: Wire.chain_flags_atoms(flags_int),
handle: handle,
rules: []
}
{family, chain}
end
defp decode_hook_attrs(attrs, family) do
case List.keyfind(attrs, nfta_chain_hook(), 0) do
{_, hook_bin} ->
hook_attrs = Attr.decode(hook_bin)
hooknum = get_u32_be(hook_attrs, nfta_hook_hooknum(), nil)
priority = get_s32_be(hook_attrs, nfta_hook_priority(), nil)
device = get_string(hook_attrs, nfta_hook_dev())
hook = if hooknum, do: Wire.hook_atom(family, hooknum), else: nil
{hook, priority, device}
nil ->
{nil, nil, nil}
end
end
# ===========================================================
# Rules
# ===========================================================
@doc """
Decodes a `NEWRULE` message body into a `%Linx.Netfilter.Rule{}`.
Returns `{family, table_name, chain_name, rule}` so the caller
can attach to the right table+chain.
"""
@spec rule(binary()) :: {Table.family(), String.t(), String.t(), Rule.t()}
def rule(body) when is_binary(body) do
{family_int, _ver, _res_id, attrs_bin} = Codec.decode_nfgenmsg(body)
family = Wire.family_atom(family_int)
attrs = Attr.decode(attrs_bin)
table_name = get_string(attrs, nfta_rule_table())
chain_name = get_string(attrs, nfta_rule_chain())
handle = get_u64_be(attrs, nfta_rule_handle(), nil)
expressions =
case List.keyfind(attrs, nfta_rule_expressions(), 0) do
{_, list_bin} -> decode_expressions(list_bin)
nil -> []
end
{tag, comment} = decode_rule_userdata(get_binary(attrs, nfta_rule_userdata()))
rule = %Rule{
expressions: expressions,
chain: chain_name,
handle: handle,
tag: tag,
comment: comment
}
{family, table_name, chain_name, rule}
end
# Mirror of the encoder's TLV format. See encoder.ex for the
# type-byte allocations.
@udata_rule_comment 0
@udata_rule_linx_tag 16
defp decode_rule_userdata(nil), do: {nil, nil}
defp decode_rule_userdata(<<>>), do: {nil, nil}
defp decode_rule_userdata(bin) do
bin
|> walk_udata({nil, nil})
end
defp walk_udata(<<>>, acc), do: acc
defp walk_udata(<<type::8, len::8, rest::binary>>, {tag, comment}) do
case rest do
<<value::binary-size(len), more::binary>> ->
str = String.trim_trailing(value, <<0>>)
new_acc =
case type do
@udata_rule_linx_tag -> {String.to_atom(str), comment}
@udata_rule_comment -> {tag, str}
_ -> {tag, comment}
end
walk_udata(more, new_acc)
_ ->
{tag, comment}
end
end
defp decode_expressions(binary) do
binary
|> Attr.decode()
|> Enum.flat_map(fn
{tag, payload} when tag == nfta_list_elem() -> [decode_expression(payload)]
_ -> []
end)
end
defp decode_expression(elem_bin) do
attrs = Attr.decode(elem_bin)
name_str = get_string(attrs, nfta_expr_name())
data_bin = get_binary(attrs, nfta_expr_data())
name_atom = expr_name_atom(name_str)
%Expr{
name: name_atom,
data: decode_expr_data(name_atom, data_bin)
}
end
defp expr_name_atom("immediate"), do: :immediate
defp expr_name_atom("cmp"), do: :cmp
defp expr_name_atom("payload"), do: :payload
defp expr_name_atom("meta"), do: :meta
defp expr_name_atom("bitwise"), do: :bitwise
defp expr_name_atom("ct"), do: :ct
defp expr_name_atom("lookup"), do: :lookup
defp expr_name_atom("reject"), do: :reject
defp expr_name_atom("counter"), do: :counter
defp expr_name_atom("nat"), do: :nat
defp expr_name_atom("masq"), do: :masq
defp expr_name_atom("redir"), do: :redir
defp expr_name_atom(other) when is_binary(other), do: other
defp decode_expr_data(_name, nil), do: nil
defp decode_expr_data(:immediate, bin) do
attrs = Attr.decode(bin)
dreg = get_u32_be(attrs, nfta_immediate_dreg(), 0)
data_bin = get_binary(attrs, nfta_immediate_data())
case decode_immediate_data(data_bin) do
%Verdict{} = v ->
if dreg == 0, do: v, else: %{dreg: dreg, value: v}
other ->
%{dreg: dreg, value: other}
end
end
defp decode_expr_data(:cmp, bin) do
attrs = Attr.decode(bin)
sreg = get_u32_be(attrs, nfta_cmp_sreg(), 1)
op_int = get_u32_be(attrs, nfta_cmp_op(), 0)
op = Wire.cmp_op_atom(op_int)
data_bin = get_binary(attrs, nfta_cmp_data())
value = decode_data_value(data_bin)
%{sreg: sreg, op: op, value: value}
end
defp decode_expr_data(:payload, bin) do
attrs = Attr.decode(bin)
dreg = get_u32_be(attrs, nfta_payload_dreg(), 1)
base_int = get_u32_be(attrs, nfta_payload_base(), 0)
base = Wire.payload_base_atom(base_int)
offset = get_u32_be(attrs, nfta_payload_offset(), 0)
len = get_u32_be(attrs, nfta_payload_len(), 0)
%{base: base, offset: offset, len: len, dreg: dreg}
end
defp decode_expr_data(:meta, bin) do
attrs = Attr.decode(bin)
dreg = get_u32_be(attrs, nfta_meta_dreg(), 1)
key_int = get_u32_be(attrs, nfta_meta_key(), 0)
%{key: Wire.meta_key_atom(key_int), dreg: dreg}
end
defp decode_expr_data(:bitwise, bin) do
attrs = Attr.decode(bin)
sreg = get_u32_be(attrs, nfta_bitwise_sreg(), 1)
dreg = get_u32_be(attrs, nfta_bitwise_dreg(), 1)
len = get_u32_be(attrs, nfta_bitwise_len(), 0)
mask = decode_data_value(get_binary(attrs, nfta_bitwise_mask())) || <<>>
xor = decode_data_value(get_binary(attrs, nfta_bitwise_xor())) || <<>>
%{sreg: sreg, dreg: dreg, len: len, mask: mask, xor: xor}
end
defp decode_expr_data(:ct, bin) do
attrs = Attr.decode(bin)
dreg = get_u32_be(attrs, nfta_ct_dreg(), 1)
key_int = get_u32_be(attrs, nfta_ct_key(), 0)
%{key: Wire.ct_key_atom(key_int), dreg: dreg}
end
defp decode_expr_data(:lookup, bin) do
attrs = Attr.decode(bin)
%{
set: get_string(attrs, nfta_lookup_set()),
sreg: get_u32_be(attrs, nfta_lookup_sreg(), 1),
dreg: get_u32_be(attrs, nfta_lookup_dreg(), nil),
flags: decode_lookup_flags(get_u32_be(attrs, nfta_lookup_flags(), 0))
}
end
defp decode_expr_data(:reject, bin) do
attrs = Attr.decode(bin)
type_int = get_u32_be(attrs, nfta_reject_type(), 0)
code = get_u8(attrs, nfta_reject_icmp_code(), nil)
%{type: Wire.reject_type_atom(type_int), code: code}
end
defp decode_expr_data(:counter, bin) do
attrs = Attr.decode(bin)
bytes = get_u64_be(attrs, nfta_counter_bytes(), 0)
packets = get_u64_be(attrs, nfta_counter_packets(), 0)
%{packets: packets, bytes: bytes}
end
defp decode_expr_data(:nat, bin) do
attrs = Attr.decode(bin)
type_int = get_u32_be(attrs, nfta_nat_type(), 0)
family_int = get_u32_be(attrs, nfta_nat_family(), 0)
flags_int = get_u32_be(attrs, nfta_nat_flags(), 0)
type = if type_int == nft_nat_dnat(), do: :dnat, else: :snat
%{
type: type,
family: Wire.family_atom(family_int),
reg_addr_min: get_u32_be(attrs, nfta_nat_reg_addr_min(), nil),
reg_addr_max: get_u32_be(attrs, nfta_nat_reg_addr_max(), nil),
reg_proto_min: get_u32_be(attrs, nfta_nat_reg_proto_min(), nil),
reg_proto_max: get_u32_be(attrs, nfta_nat_reg_proto_max(), nil),
flags: Wire.nat_flags_atoms(flags_int)
}
end
defp decode_expr_data(:masq, bin) do
attrs = Attr.decode(bin)
flags_int = get_u32_be(attrs, nfta_masq_flags(), 0)
%{
flags: Wire.nat_flags_atoms(flags_int),
reg_proto_min: get_u32_be(attrs, nfta_masq_reg_proto_min(), nil),
reg_proto_max: get_u32_be(attrs, nfta_masq_reg_proto_max(), nil)
}
end
defp decode_expr_data(:redir, bin) do
attrs = Attr.decode(bin)
flags_int = get_u32_be(attrs, nfta_redir_flags(), 0)
%{
flags: Wire.nat_flags_atoms(flags_int),
reg_proto_min: get_u32_be(attrs, nfta_redir_reg_proto_min(), nil),
reg_proto_max: get_u32_be(attrs, nfta_redir_reg_proto_max(), nil)
}
end
defp decode_expr_data(_name, bin), do: bin
defp decode_immediate_data(nil), do: nil
defp decode_immediate_data(bin) do
attrs = Attr.decode(bin)
case List.keyfind(attrs, nfta_data_verdict(), 0) do
{_, verdict_bin} ->
decode_verdict(verdict_bin)
nil ->
# Non-verdict value (e.g. constant load into a register)
get_binary(attrs, nfta_data_value())
end
end
defp decode_data_value(nil), do: nil
defp decode_data_value(bin) do
attrs = Attr.decode(bin)
get_binary(attrs, nfta_data_value())
end
defp decode_verdict(bin) do
attrs = Attr.decode(bin)
code = get_s32_be(attrs, nfta_verdict_code(), 0)
chain = get_string(attrs, nfta_verdict_chain())
kind = Wire.verdict_atom(code)
target = if kind in [:jump, :goto], do: chain, else: nil
%Verdict{kind: kind, target: target}
end
defp decode_lookup_flags(0), do: []
defp decode_lookup_flags(int) do
import Bitwise
if (int &&& 1) != 0, do: [:inv], else: []
end
# ===========================================================
# Sets / maps
# ===========================================================
@doc """
Decodes a `NEWSET` body into either a `%Linx.Netfilter.Set{}`
(plain set — no `NFT_SET_F_MAP` flag) or a `%Linx.Netfilter.Map{}`
(map / vmap).
Returns `{family, set_or_map}` for downstream assembly.
"""
@spec set(binary()) :: {Table.family(), Set.t() | NMap.t()}
def set(body) when is_binary(body) do
{family_int, _ver, _res_id, attrs_bin} = Codec.decode_nfgenmsg(body)
family = Wire.family_atom(family_int)
attrs = Attr.decode(attrs_bin)
name = get_string(attrs, nfta_set_name())
table = get_string(attrs, nfta_set_table())
flags_int = get_u32_be(attrs, nfta_set_flags(), 0)
flags = Wire.set_flags_atoms(flags_int)
key_type_int = get_u32_be(attrs, nfta_set_key_type(), 0)
key_len = get_u32_be(attrs, nfta_set_key_len(), 0)
key_type = Wire.set_type_atom(key_type_int, key_len)
is_map? = Enum.member?(flags, :map)
is_anon? = Enum.member?(flags, :anonymous)
timeout = get_u64_be(attrs, nfta_set_timeout(), nil)
gc_interval = get_u32_be(attrs, nfta_set_gc_interval(), nil)
handle = get_u64_be(attrs, nfta_set_handle(), nil)
size = decode_set_desc_size(attrs)
user_flags = Enum.reject(flags, &(&1 in [:map, :anonymous]))
entity =
if is_map? do
data_type_int = get_u32_be(attrs, nfta_set_data_type(), 0)
data_len = get_u32_be(attrs, nfta_set_data_len(), 0)
data_type =
cond do
data_type_int == 0xFFFFFF00 -> :verdict
true -> Wire.set_type_atom(data_type_int, data_len)
end
%NMap{
name: name,
table: table,
key_type: key_type,
data_type: data_type,
flags: user_flags ++ if(is_anon?, do: [:anonymous], else: []),
elements: [],
timeout: timeout,
gc_interval: gc_interval,
size: size,
handle: handle,
comment: nil
}
else
%Set{
name: name,
table: table,
key_type: key_type,
flags: user_flags ++ if(is_anon?, do: [:anonymous], else: []),
elements: [],
timeout: timeout,
gc_interval: gc_interval,
size: size,
handle: handle,
comment: nil
}
end
{family, entity}
end
defp decode_set_desc_size(attrs) do
case List.keyfind(attrs, nfta_set_desc(), 0) do
{_, desc_bin} ->
desc_attrs = Attr.decode(desc_bin)
get_u32_be(desc_attrs, nfta_set_desc_size(), nil)
nil ->
nil
end
end
@doc """
Decodes a `NEWSETELEM` body into a list of elements attached to
a `(family, table_name, set_name)`.
Returns `{family, table_name, set_name, elements}` where
`elements` is a list of either raw key terms or `{key, value}`
tuples (the caller resolves which based on whether the parent
set is plain or a map).
For now we return the elements with KEY binary unparsed (raw
binary) and DATA either a raw binary or a `%Verdict{}` for
verdict data. Higher-level conversion (binary → tuple, etc.)
happens at assembly time when we know the parent set's
key_type.
"""
@spec set_elements(binary()) ::
{Table.family(), String.t(), String.t(), [{binary(), term()} | binary()]}
def set_elements(body) when is_binary(body) do
{family_int, _ver, _res_id, attrs_bin} = Codec.decode_nfgenmsg(body)
family = Wire.family_atom(family_int)
attrs = Attr.decode(attrs_bin)
table = get_string(attrs, nfta_set_elem_list_table())
set_name = get_string(attrs, nfta_set_elem_list_set())
elements =
case List.keyfind(attrs, nfta_set_elem_list_elements(), 0) do
{_, list_bin} ->
list_bin
|> Attr.decode()
|> Enum.flat_map(fn
{tag, payload} when tag == nfta_list_elem() -> [decode_one_set_elem(payload)]
_ -> []
end)
nil ->
[]
end
{family, table, set_name, elements}
end
defp decode_one_set_elem(bin) do
attrs = Attr.decode(bin)
key_bin =
case List.keyfind(attrs, nfta_set_elem_key(), 0) do
{_, key_nla} ->
key_attrs = Attr.decode(key_nla)
get_binary(key_attrs, nfta_data_value())
nil ->
nil
end
case List.keyfind(attrs, nfta_set_elem_data(), 0) do
{_, data_nla} ->
data_attrs = Attr.decode(data_nla)
data_value =
cond do
verdict_bin = get_binary(data_attrs, nfta_data_verdict()) ->
decode_verdict(verdict_bin)
true ->
get_binary(data_attrs, nfta_data_value())
end
{key_bin, data_value}
nil ->
key_bin
end
end
@doc """
Materialises a raw set-element list (from `set_elements/1`) into
the value shape the parent set expects. Plain sets keep raw key
binaries (the codec doesn't know to expand `<<10, 0, 0, 5>>` back
to `{10, 0, 0, 5}` without context). Maps preserve `{key, value}`.
"""
@spec materialize_elements([{binary(), term()} | binary()], atom(), atom() | nil) ::
[term()]
def materialize_elements(elements, key_type, data_type) do
Enum.map(elements, fn
{k, v} -> {decode_key(k, key_type), decode_data(v, data_type)}
k -> decode_key(k, key_type)
end)
end
defp decode_key(<<a, b, c, d>>, :ipv4_addr), do: {a, b, c, d}
defp decode_key(<<a::16, b::16, c::16, d::16, e::16, f::16, g::16, h::16>>, :ipv6_addr),
do: {a, b, c, d, e, f, g, h}
defp decode_key(<<a, b, c, d, e, f>>, :ether_addr), do: {a, b, c, d, e, f}
defp decode_key(<<port::big-16>>, :inet_service), do: port
defp decode_key(<<proto>>, :inet_proto), do: proto
defp decode_key(<<mark::big-32>>, :mark), do: mark
defp decode_key(bin, :ifname), do: String.trim_trailing(bin, <<0>>)
defp decode_key(bin, _), do: bin
defp decode_data(%Verdict{} = v, :verdict), do: v
defp decode_data(bin, type), do: decode_key(bin, type)
# ===========================================================
# Assembly
# ===========================================================
@doc """
Builds a `%Ruleset{}` from separate lists of decoded entries.
* `tables` — `[%Table{}]` from a `NFT_MSG_GETTABLE` dump.
* `chains` — `[{family, %Chain{}}]` from a `NFT_MSG_GETCHAIN` dump.
* `rules` — `[{family, table_name, chain_name, %Rule{}}]` from
a `NFT_MSG_GETRULE` dump.
* `sets` — `[{family, %Set{} | %Map{}}]` from a
`NFT_MSG_GETSET` dump.
* `set_elements` — `[{family, table_name, set_name, [elem]}]`
from per-set `NFT_MSG_GETSETELEM` calls.
Chains, sets, and rules are attached to their parents by
`(family, table_name)`. Set elements are materialised against
the parent set's `key_type` / `data_type` and attached to the
set in dump order.
Entities that reference a missing parent are silently dropped.
"""
@spec from_msgs(
[Table.t()],
[{Table.family(), Chain.t()}],
[{Table.family(), String.t(), String.t(), Rule.t()}],
[{Table.family(), Set.t() | NMap.t()}],
[{Table.family(), String.t(), String.t(), [term()]}]
) :: Ruleset.t()
def from_msgs(tables, chains, rules, sets \\ [], set_elements \\ []) do
tables_map =
Enum.reduce(tables, %{}, fn %Table{family: f, name: n} = t, acc ->
Map.put(acc, {f, n}, t)
end)
tables_map = attach_chains(tables_map, chains)
tables_map = attach_sets(tables_map, sets)
tables_map = attach_set_elements(tables_map, set_elements)
tables_map = attach_rules(tables_map, rules)
%Ruleset{tables: tables_map}
end
defp attach_sets(tables_map, sets) do
Enum.reduce(sets, tables_map, fn {family, entity}, acc ->
key = {family, entity_table(entity)}
case Map.fetch(acc, key) do
{:ok, %Table{} = t} ->
updated =
case entity do
%Set{} ->
%Table{t | sets: Map.put(t.sets, entity.name, entity)}
%NMap{} ->
%Table{t | maps: Map.put(t.maps, entity.name, entity)}
end
Map.put(acc, key, updated)
:error ->
acc
end
end)
end
defp entity_table(%Set{table: t}), do: t
defp entity_table(%NMap{table: t}), do: t
defp attach_set_elements(tables_map, set_elements) do
Enum.reduce(set_elements, tables_map, fn {family, table_name, set_name, raw_elems}, acc ->
key = {family, table_name}
case Map.fetch(acc, key) do
{:ok, %Table{} = t} ->
cond do
Map.has_key?(t.sets, set_name) ->
%Set{} = set = Map.fetch!(t.sets, set_name)
materialised = materialize_elements(raw_elems, set.key_type, nil)
updated_set = %Set{set | elements: set.elements ++ materialised}
Map.put(acc, key, %Table{t | sets: Map.put(t.sets, set_name, updated_set)})
Map.has_key?(t.maps, set_name) ->
%NMap{} = map = Map.fetch!(t.maps, set_name)
materialised = materialize_elements(raw_elems, map.key_type, map.data_type)
updated_map = %NMap{map | elements: map.elements ++ materialised}
Map.put(acc, key, %Table{t | maps: Map.put(t.maps, set_name, updated_map)})
true ->
acc
end
:error ->
acc
end
end)
end
defp attach_chains(tables_map, chains) do
Enum.reduce(chains, tables_map, fn {family, %Chain{} = chain}, acc ->
key = {family, chain.table}
case Map.fetch(acc, key) do
{:ok, %Table{} = t} ->
Map.put(acc, key, %Table{t | chains: Map.put(t.chains, chain.name, chain)})
:error ->
acc
end
end)
end
defp attach_rules(tables_map, rules) do
Enum.reduce(rules, tables_map, fn {family, table_name, chain_name, %Rule{} = rule}, acc ->
key = {family, table_name}
with {:ok, %Table{} = t} <- Map.fetch(acc, key),
{:ok, %Chain{} = c} <- Map.fetch(t.chains, chain_name) do
updated_chain = %Chain{c | rules: c.rules ++ [rule]}
updated_table = %Table{t | chains: Map.put(t.chains, chain_name, updated_chain)}
Map.put(acc, key, updated_table)
else
_ -> acc
end
end)
end
# ===========================================================
# Multicast events
# ===========================================================
@doc """
Decodes a `NFNLGRP_NFTABLES` multicast message into a partial
`%Linx.Netfilter.Event{}` — `gen_id` / `proc_pid` / `proc_name`
are left nil; the Monitor GenServer fills them in from the most
recent `NEW_GEN` event.
For NEW_GEN events, the gen / pid / name come from the body
itself.
Dispatches on the low byte of `nlmsghdr.type` (the per-subsys
message opcode).
"""
@spec event(Message.t()) :: Event.t()
def event(%Message{type: type, payload: body}) do
{_subsys, msg_type} = Codec.split_type(type)
decode_event(msg_type, body)
end
defp decode_event(msg_type, body) do
cond do
msg_type == nft_msg_newgen() ->
gen = decode_gen(body)
%Event{
op: :new_gen,
entity: gen,
gen_id: gen.id,
proc_pid: gen.proc_pid,
proc_name: gen.proc_name
}
msg_type == nft_msg_newtable() ->
%Event{op: :new_table, entity: table(body)}
msg_type == nft_msg_deltable() or msg_type == nft_msg_destroytable() ->
%Event{op: :del_table, entity: table(body)}
msg_type == nft_msg_newchain() ->
%Event{op: :new_chain, entity: chain(body)}
msg_type == nft_msg_delchain() or msg_type == nft_msg_destroychain() ->
%Event{op: :del_chain, entity: chain(body)}
msg_type == nft_msg_newrule() ->
%Event{op: :new_rule, entity: rule(body)}
msg_type == nft_msg_delrule() or msg_type == nft_msg_destroyrule() ->
%Event{op: :del_rule, entity: rule(body)}
msg_type == nft_msg_newset() ->
%Event{op: :new_set, entity: set(body)}
msg_type == nft_msg_delset() or msg_type == nft_msg_destroyset() ->
%Event{op: :del_set, entity: set(body)}
msg_type == nft_msg_newsetelem() ->
%Event{op: :new_set_element, entity: set_elements(body)}
msg_type == nft_msg_delsetelem() or msg_type == nft_msg_destroysetelem() ->
%Event{op: :del_set_element, entity: set_elements(body)}
true ->
%Event{op: {:unknown, msg_type}, entity: body}
end
end
defp decode_gen(body) do
{_family, _ver, _res_id, attrs_bin} = Codec.decode_nfgenmsg(body)
attrs = Attr.decode(attrs_bin)
%{
id: get_u32_be(attrs, nfta_gen_id(), 0),
proc_pid: get_u32_be(attrs, nfta_gen_proc_pid(), nil),
proc_name: decode_gen_proc_name(attrs)
}
end
defp nfta_gen_id, do: 1
defp nfta_gen_proc_pid, do: 2
defp nfta_gen_proc_name, do: 3
defp decode_gen_proc_name(attrs) do
case List.keyfind(attrs, nfta_gen_proc_name(), 0) do
{_, value} -> String.trim_trailing(value, <<0>>)
nil -> nil
end
end
# ===========================================================
# Attribute lookup helpers
# ===========================================================
defp get_string(attrs, tag) do
case List.keyfind(attrs, tag, 0) do
{^tag, value} -> String.trim_trailing(value, <<0>>)
nil -> nil
end
end
defp get_binary(attrs, tag) do
case List.keyfind(attrs, tag, 0) do
{^tag, value} -> value
nil -> nil
end
end
defp get_u32_be(attrs, tag, default) do
case List.keyfind(attrs, tag, 0) do
{^tag, <<v::big-unsigned-32>>} -> v
_ -> default
end
end
defp get_u64_be(attrs, tag, default) do
case List.keyfind(attrs, tag, 0) do
{^tag, <<v::big-unsigned-64>>} -> v
_ -> default
end
end
defp get_s32_be(attrs, tag, default) do
case List.keyfind(attrs, tag, 0) do
{^tag, <<v::big-signed-32>>} -> v
_ -> default
end
end
defp get_u8(attrs, tag, default) do
case List.keyfind(attrs, tag, 0) do
{^tag, <<v::unsigned-8>>} -> v
_ -> default
end
end
# Userdata is opaque on the wire; the Table struct doesn't have a
# dedicated slot for it yet. For now, silently swallow.
defp maybe_attach_userdata(table, nil), do: table
defp maybe_attach_userdata(table, _bin), do: table
end