Binary Range Expression (BREX) is a Domain Specific Language (DSL) for extracting sub-ranges from binary data based on position and/or conditions, with support for Type-Length-Value (TLV) searches. While designed for network packet analysis and processing, BREX can be used for security scanning, file identification, and other binary data manipulation tasks.
Note: BREX expressions are used in various Proxylity UDP Gateway destination configurations, including SQS and IoT Data destinations, to extract specific data from UDP packet payloads.
Basic Memory Access
Simple Byte Access
[5] - Returns byte at offset 5
[2, 4] - Returns 4 bytes starting at offset 2
[0, 10] - Returns first 10 bytes
Range Access with Colons
[2:6] - Returns 4 bytes starting at offset 2 up to but not including byte 6
[0:10] - Returns first 10 bytes
[2:] - Return all bytes starting with the byte at offset 2
[:] - Returns all bytes
[] - Returns an empty range
Offset Expressions
[u8[0] + 1] - Read byte at (value of byte 0) + 1
[u16le[0] * 2, 4] - Read 4 bytes at ((16-bit value at byte 0) * 2)
[u8[0] + 1, 10] | [5] - Read 10 bytes from calculated offset, get byte 5
Expression Chaining
Expression chaining allows you to pass the result of one operation as input to the next operation, creating powerful data processing pipelines. BREX uses the pipe operator | to chain expressions explicitly.
The Pipe Operator
The pipe operator | takes the result from the left expression and makes it the data context for the right expression:
[10:20] | [5] - Get bytes 10-19, then get byte 5 of that result
[0:100] | [50:60] - Get bytes 0-99, then get bytes 50-59 of that result
[0:100] | [50:60] | [0] - Multi-step: get range, then sub-range, then first byte
Left-to-Right Data Flow
Chaining evaluates from left to right, where each operation receives the output of the previous operation:
[20:30] | [2:8] | [1]
Step-by-step evaluation:
[20:30] → produces 10 bytes (original buffer bytes 20-29)
| [2:8] → produces 6 bytes (bytes 2-7 of the 10-byte result)
| [1] → produces 1 byte (byte 1 of the 6-byte result)
When to Use Chaining
Chaining is particularly useful for:
- Structured data parsing: Extracting parts of TLV records, headers, etc.
- Multi-step filtering: Progressive narrowing of data ranges
- Protocol decoding: Following offset chains and nested structures
- Data validation: Extracting and checking multiple related fields
[0:20] | [16:] | u32le[0:] - Get header, skip to timestamp field, read as uint32
Slice Search Operations
Slice search operations allow you to iterate through structured data like TLV (Type-Length-Value) records, finding entries that match specific conditions. By default, slice search returns the entire matching slice. Use chained range operations to extract specific parts.
Basic TLV Search
[20:, ==31] - Find entire TLV slice where type=31 starting at position 20
[20:, [0]=31] - Same as above but explicit (relative addressing)
[0:, ==0x42] - Find entire TLV slice where type=0x42 from start of buffer
Assumes: type at [0], length at [1], both relative to slice
Extracting Parts with Chaining
[20:, [0]==31] | [2:] - Find TLV type 31, return value part (skip type+length)
[20:, [0]==31] | [0] - Find TLV type 31, return just the type byte
[20:, [0]==31] | [1] - Find TLV type 31, return just the length byte
[20:, [0]==31] | [0:2] - Find TLV type 31, return type+length
[20:, [0]==31] | [2:] | [0] - First byte of value part
Advanced Conditions
[20:, [0]>10] - Find slice where type > 10
[20:, [0]==31 && [2]<5] - Type=31 AND first value byte < 5
[20:, [0]==31 || [0]==32] - Type 31 OR type 32
[20:, [0] & 0x80 == 0] - Type field bitmask check
[20:, [0]==31 && [@100]>10] - Type=31 AND buffer position 100 > 10
Custom Length Field Locations
[20:, [0]==31] - Standard: length at [1] (default, relative)
[20:, [1]==31, [0]] - Length-first: length at [0], type at [1]
[20:, [0]==31, [2]] - Type at [0], length at [2]
[20:, [0]==31, u16be[2:]] - Type=31, u16be length at relative position 2
[20:, [0]==31, u32le[4:]] - Type at [0], u32le length at relative position 4
Fixed-Size Records
[20:, [0]==31, 8] - 8-byte records, find where first byte = 31
[0:, [2]>100, 12] - 12-byte records, find where 3rd byte > 100
[20:, [0]==0x42, 16] - 16-byte records, find type 0x42
[20:, [0]==31, 8] | [1:7] - Skip first byte, get remaining 6 bytes
Absolute Addressing with @
The @ prefix provides absolute addressing to the original buffer, while operations without @ are relative to the current slice context:
[0] - Byte 0 of current slice (typically type in TLV)
[1] - Byte 1 of current slice (typically length in TLV)
[@0] - Position 0 in the original buffer
[@100] - Position 100 in the original buffer
Complete TLV Examples
Standard RADIUS TLV
Format: [type:1][length:1][value:N]
[20:, [0]==31] - Find entire TLV slice for type 31
[20:, [0]==31] | [2:] - Find type 31, return value part only
[20:, [0]==31] | [1] - Find type 31, return length byte
Custom TLV with 16-bit Length
Format: [type:1][reserved:1][length:2be][value:N]
[0:, [0]==0x42, u16be[2:]] - Find type 0x42 with u16be length at relative [2]
[0:, [0]==0x42, u16be[2:]] | [4:] - Same, but return value part (skip 4-byte header)
[0:, [0]==0x42, u16be[2:]] | [2:4] - Same, but return just the length field
Length-First Format
Format: [length:2le][type:1][flags:1][value:N]
[20:, [2]==31, u16le[0:]] - Length at [0], type at [2] (both relative)
[20:, [2]==31, u16le[0:]] | [4:] - Same, return value part (skip 4-byte header)
[20:, [2]==31, u16le[0:]] | [2:4] - Same, return type+flags
Data Types
Integer interpretations of byte sequences can be specified for indexes, lengths, and condition operands.
8-bit Types
u8[0] - Unsigned 8-bit integer at offset 0
i8[0] - Signed 8-bit integer at offset 0
16-bit Types
u16le[0:] - Unsigned 16-bit little-endian
u16be[0:] - Unsigned 16-bit big-endian
i16le[0:] - Signed 16-bit little-endian
i16be[0:] - Signed 16-bit big-endian
32-bit Types
u32le[0:] - Unsigned 32-bit little-endian
u32be[0:] - Unsigned 32-bit big-endian
i32le[0:] - Signed 32-bit little-endian
i32be[0:] - Signed 32-bit big-endian
64-bit Types
u64le[0:] - Unsigned 64-bit little-endian
u64be[0:] - Unsigned 64-bit big-endian
i64le[0:] - Signed 64-bit little-endian
i64be[0:] - Signed 64-bit big-endian
Endianness Behavior
The endianness specified in the type is preserved throughout expression evaluation:
Given data: byte[] { 0x01, 0x02 }
u16be[0:] == 258 - true (0x01 << 8 + 0x02 = 258)
u16le[0:] == 513 - true (0x01 + 0x02 << 8 = 513)
Dynamic Indexing
Single-Level Indexing
[u8[0]] - Read byte at offset specified by byte 0
[u16le[2:], u8[0]] - Read (byte 0) bytes at offset (16-bit value at 2)
Multi-Level Indexing (Pointer Following)
[u8[u8[0]]] - Three-level pointer following
[u8[u16le[0:]]] - Mixed types in pointer chain
Indexing with Arithmetic
[u8[0] + 4] - Read at (byte 0 value) + 4
[u8[0] * 2, u8[1]] - Dynamic offset and length
Arithmetic Operations
Basic Operations
[u8[0] + u8[1]] - Add two byte values
u16le[0:] - 100 - Subtract constant
u8[0] * 4 - Multiply by constant
u32le[0:] / u16le[4:] - Divide two values
u8[0] % 16 - Modulo operation
Type Mixing
All arithmetic operations convert to the largest type involved:
u8[0] + u16le[1] - Result is 16-bit
u16le[0:] + u32be[2:] - Result is 32-bit
Conditional Expressions
Ternary Operator
u8[0] > 10 ? [1, u8[0]] : [0, 1] - condition ? true_expression : false_expression
u8[0] == 1 ? [1, 4] : u8[0] == 2 ? [5, 8] : [0, 1] - Nested conditions
Comparison Operators
u8[0] == 42 - Equality
u8[0] != 0 - Inequality
u16le[0:] > 1000 - Greater than
u16le[0:] >= 1000 - Greater than or equal
u8[0] < 128 - Less than
u8[0] <= 127 - Less than or equal
Null Coalescing
u8[100] ?? [0, 1] - Return [0,1] if offset 100 is out of bounds
Range Bitwise Operations
When applied to ranges, &&, ||, and ^^ perform byte-wise bitwise operations:
[0:2] && [2:4] - Bitwise AND between two ranges
[0:2] || [2:4] - Bitwise OR between two ranges
[0:2] ^^ [2:4] - Bitwise XOR between two ranges
[0] && [1] - Single byte AND operation
!(u8[0] > 128) - Logical NOT (for conditions)
Note: For mismatched range lengths, operations use truncation (only overlapping byte positions are used).
Concise Syntax
BREX provides several shorthand forms to make common expressions more concise.
Implicit Chaining
Instead of explicit pipe operators, you can chain operations by placing brackets directly adjacent:
Verbose: [10:20] | [5]
Concise: [10:20][5]
Verbose: [20:, [0]==31] | [2:]
Concise: [20:, [0]==31][2:]
Multi-step verbose: [20:, [0]==31] | [2:] | [0:4]
Multi-step concise: [20:, [0]==31][2:][0:4]
Concise Search Conditions
Simple equality conditions can omit the explicit field reference:
Verbose: [20:, [0]==31]
Concise: [20:, ==31] (assumes [0]==31)
Verbose: [20:, [0]==0x42]
Concise: [20:, ==0x42] (assumes [0]==0x42)
When to Use Concise vs Verbose
Use verbose syntax when:
- Learning BREX for the first time
- Code needs to be very clear and self-documenting
- Working with complex multi-step operations
- Collaboration requires maximum readability
Use concise syntax when:
- Writing quick scripts or prototypes
- Familiar with BREX patterns
- Space/brevity is important
- Working with well-understood data formats
Operator Precedence
From highest to lowest precedence:
- Primary expressions:
[offset],type[offset], literals, parentheses - Chained indexing:
expr[range],expr[offset](left-associative) - Unary:
!,-(unary minus),@ - Multiplicative:
*,/,% - Additive:
+,- - Comparison:
<,<=,>,>= - Equality:
==,!= - Pipe:
|(expression chaining, left-associative) - Range Bitwise XOR:
^^ - Range Bitwise AND:
&& - Range Bitwise OR:
|| - Null coalescing:
?? - Ternary conditional:
? :
Precedence Examples
u8[0] + u8[1] * 2 is equivalent to u8[0] + (u8[1] * 2)
[0] && [1] ^^ [2] is equivalent to ([0] && [1]) ^^ [2]
u8[0] == 1 ? 2 : 3 + 4 is equivalent to u8[0] == 1 ? 2 : (3 + 4)
[20:, ==31] | [2:] | [0] + 5 is equivalent to (([20:, ==31] | [2:]) | [0]) + 5
Best Practices
1. Use Appropriate Types
Good: Explicit about endianness
u16le[0:] + u16le[2:]
Avoid: Implicit typing unnecessarily
u16le[0:] + [2:] (works but may not be intended)
2. Bounds Checking
2. Bounds Checking
Good: Safe access with fallback
u8[1] >= 6 ? u32le[u8[2]:] : 0
Also Good: Use null coalescing
u32le[u8[2]:] ?? 0
3. Readable Complex Expressions
Good: Clear intent
u8[0] == 0x04 ? [1, u8[0]] : [0, 1]
Consider breaking complex expressions into steps:
[20:, [0]==31][2:] - First find TLV, then extract value
4. Error Handling
- Out of Bounds Access: Use null coalescing operator
??to provide fallback values - Type Conversion Errors: Ensure sufficient bytes exist for multi-byte value reads
- Division by Zero: Validate denominators before division operations