BPF (BPF v0.2.0)

View Source

A library for compiling Elixir binary pattern matching expressions into classic BPF programs.

Compiling produces a %BPF.Program{} struct containing the BPF instructions and variable bindings. The program can be interpreted against binary packets (for testing), or assembled into raw BPF bytecode for use with sockets or libpcap.

Example

iex> prog = BPF.compile(fn <<4::4, ihl::4, _::binary>> when ihl >= 5 -> true end)
%BPF.Program{
  instructions: [
    {:ld, :b, [:k, 0]},
    {:rsh, 4},
    {:and, 15},
    {:jmp, :jeq, :k, 4, 0, 4},
    {:ld, :b, [:k, 0]},
    {:and, 15},
    {:jmp, :jge, :k, 5, 0, 1},
    {:ret, :k, 4294967295},
    {:ret, :k, 0}
  ],
  bindings: %{ihl: 4}
}
iex> BPF.interpret(prog, <<0x45, 0x00, 0x00, 0x14>>)
true
iex> BPF.assemble(prog)
<<48, 0, 0, 0, 0, 0, 0, 0, 116, 0, 0, 0, 4, 0, 0, 0, 84, 0, 0, 0, 15, 0, 0, 0,
  21, 0, 0, 4, 4, 0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 84, 0, 0, 0, 15, 0, 0, 0,
  53, 0, 0, 1, 5, 0, 0, 0, 6, 0, 0, 0, 255, 255, 255, 255, 6, 0, 0, 0, 0, 0, 0,
  0>>

Summary

Functions

Assemble a BPF program into raw bytecode.

Compile a binary pattern matching function into a BPF program.

Interpret a BPF program against a binary packet.

Functions

assemble(program)

Assemble a BPF program into raw bytecode.

Returns a binary suitable for use with SO_ATTACH_FILTER or other kernel interfaces that accept classic BPF bytecode.

Each instruction is encoded as 8 bytes in little-endian format:

  • 2 bytes: opcode
  • 1 byte: jt (jump if true offset)
  • 1 byte: jf (jump if false offset)
  • 4 bytes: k (immediate constant)

Examples

iex> prog = BPF.compile(fn <<x::8>> when x == 42 -> true end)
iex> bytes = BPF.assemble(prog)
iex> is_binary(bytes) and rem(byte_size(bytes), 8) == 0
true

compile(expr)

(macro)

Compile a binary pattern matching function into a BPF program.

Takes a quoted anonymous function with binary pattern matching and optional guards, and compiles it to a %BPF.Program{} struct.

Examples

# Simple byte match - returns a BPF.Program struct
iex> BPF.compile(fn <<x::8>> when x == 42 -> true end)
%BPF.Program{
  instructions: [
    {:ld, :b, [:k, 0]},
    {:jmp, :jeq, :k, 42, 0, 1},
    {:ret, :k, 4294967295},
    {:ret, :k, 0}
  ],
  bindings: %{x: 0}
}

# IPv4 packet filter
iex> prog = BPF.compile(fn <<4::4, ihl::4, _::binary>> when ihl >= 5 -> true end)
%BPF.Program{
  instructions: [
    {:ld, :b, [:k, 0]},
    {:rsh, 4},
    {:and, 15},
    {:jmp, :jeq, :k, 4, 0, 4},
    {:ld, :b, [:k, 0]},
    {:and, 15},
    {:jmp, :jge, :k, 5, 0, 1},
    {:ret, :k, 4294967295},
    {:ret, :k, 0}
  ],
  bindings: %{ihl: 4}
}
iex> BPF.interpret(prog, <<0x45, 0x00, 0x00, 0x28>>)
true

# Multi-clause: accept IPv4 or IPv6 packets over 100 bytes
# IPv4: version=4, total length at bytes 2-3
# IPv6: version=6, payload length at bytes 4-5 (add 40 for header)
iex> prog = BPF.compile(fn
...>   <<4::4, _ihl::4, _tos::8, len::16, _::binary>> when len > 100 -> true
...>   <<6::4, _::28, len::16, _::binary>> when len + 40 > 100 -> true
...> end)
%BPF.Program{
  instructions: [
    {:ld, :b, [:k, 0]},
    {:rsh, 4},
    {:and, 15},
    {:jmp, :jeq, :k, 4, 0, 3},
    {:ld, :h, [:k, 2]},
    {:jmp, :jgt, :k, 100, 0, 1},
    {:ret, :k, 4294967295},
    {:ld, :b, [:k, 0]},
    {:rsh, 4},
    {:and, 15},
    {:jmp, :jeq, :k, 6, 0, 4},
    {:ld, :h, [:k, 4]},
    {:add, 40},
    {:jmp, :jgt, :k, 100, 0, 1},
    {:ret, :k, 4294967295},
    {:ret, :k, 0}
  ],
  bindings: %{len: 32, _ihl: 4, _tos: 8}
}
iex> BPF.interpret(prog, <<0x45, 0x00, 0x00, 0x96, 0x00>>)  # IPv4, length 150
true
iex> BPF.interpret(prog, <<0x60, 0x00, 0x00, 0x00, 0x00, 0x50>>)  # IPv6, payload 80
true
iex> BPF.interpret(prog, <<0x45, 0x00, 0x00, 0x32, 0x00>>)  # IPv4, length 50
false

# Single clause with conditional return: accept IPv4 or IPv6 packets
iex> prog = BPF.compile(fn <<version::4, _::binary>> -> version == 4 or version == 6 end)
%BPF.Program{
  instructions: [
    {:ld, :b, [:k, 0]},
    {:rsh, 4},
    {:and, 15},
    {:st, 0},
    {:jmp, :jeq, :k, 4, 1, 0},
    {:jmp, :jeq, :k, 6, 0, 1},
    {:ret, :k, 4294967295},
    {:ret, :k, 0}
  ],
  bindings: %{version: 0}
}
iex> BPF.interpret(prog, <<0x45, 0x00>>)  # IPv4
true
iex> BPF.interpret(prog, <<0x60, 0x00>>)  # IPv6
true
iex> BPF.interpret(prog, <<0x50, 0x00>>)  # version 5
false

# Filter by packet length using byte_size/1
# The packet variable must be bound with = to use byte_size
iex> prog = BPF.compile(fn <<_::binary>> = packet when byte_size(packet) >= 64 -> true end)
%BPF.Program{
  instructions: [
    {:ld, :len},
    {:jmp, :jge, :k, 64, 0, 1},
    {:ret, :k, 4294967295},
    {:ret, :k, 0}
  ],
  bindings: %{}
}
iex> BPF.interpret(prog, :binary.copy(<<0>>, 64))
true
iex> BPF.interpret(prog, :binary.copy(<<0>>, 63))
false

# Combine pattern matching with packet length check
iex> prog = BPF.compile(fn <<4::4, _::4, _::binary>> = pkt when byte_size(pkt) >= 20 -> true end)
%BPF.Program{
  instructions: [
    {:ld, :b, [:k, 0]},
    {:rsh, 4},
    {:and, 15},
    {:jmp, :jeq, :k, 4, 0, 3},
    {:ld, :len},
    {:jmp, :jge, :k, 20, 0, 1},
    {:ret, :k, 4294967295},
    {:ret, :k, 0}
  ],
  bindings: %{}
}
iex> BPF.interpret(prog, <<0x45>> <> :binary.copy(<<0>>, 19))  # IPv4, 20 bytes
true
iex> BPF.interpret(prog, <<0x45, 0x00>>)  # IPv4, but too short
false

Supported Patterns

  • Literal values: <<4::4, 0x0800::16>>
  • Bindings: <<version::4, ihl::4, tos::8>>
  • Skips: <<_::8, _::binary>>
  • Size modifiers: <<x::16-little>>

Supported Guards

  • Comparisons: ==, !=, <, >, <=, >=
  • Logical: and, or
  • Bitwise: band/2, bor/2, bxor/2
  • Arithmetic: +, -, *, div/2
  • Packet length: byte_size(packet) when packet is bound with = packet

interpret(program, packet)

Interpret a BPF program against a binary packet.

Returns true if the program accepts the packet (non-zero return value), or false if it rejects (zero return value).

Examples

iex> prog = BPF.compile(fn <<x::8>> when x == 42 -> true end)
iex> BPF.interpret(prog, <<42>>)
true

iex> prog = BPF.compile(fn <<x::8>> when x == 42 -> true end)
iex> BPF.interpret(prog, <<99>>)
false