From 818e12a1ebdcf013a6e4dbc5731ce166e2d10270 Mon Sep 17 00:00:00 2001 From: MuriloChianfa Date: Thu, 29 Jan 2026 00:41:45 -0300 Subject: [PATCH] add lua bindings with luarocks packaging support --- .github/workflows/ci.yml | 35 +- CMakeLists.txt | 11 + bindings/lua/CMakeLists.txt | 208 +++++++ bindings/lua/Makefile | 113 ++++ bindings/lua/README.md | 437 +++++++++++++ bindings/lua/examples/basic_example.lua | 208 +++++++ bindings/lua/examples/batch_example.lua | 336 ++++++++++ bindings/lua/examples/ipv6_example.lua | 231 +++++++ bindings/lua/liblpm-2.0.0-1.rockspec | 79 +++ bindings/lua/src/liblpm.c | 794 ++++++++++++++++++++++++ bindings/lua/src/liblpm_utils.c | 353 +++++++++++ bindings/lua/tests/test_lpm.lua | 680 ++++++++++++++++++++ docker/Dockerfile.lua | 129 ++++ docker/README.md | 33 + scripts/docker-build.sh | 7 +- 15 files changed, 3651 insertions(+), 3 deletions(-) create mode 100644 bindings/lua/CMakeLists.txt create mode 100644 bindings/lua/Makefile create mode 100644 bindings/lua/README.md create mode 100644 bindings/lua/examples/basic_example.lua create mode 100644 bindings/lua/examples/batch_example.lua create mode 100644 bindings/lua/examples/ipv6_example.lua create mode 100644 bindings/lua/liblpm-2.0.0-1.rockspec create mode 100644 bindings/lua/src/liblpm.c create mode 100644 bindings/lua/src/liblpm_utils.c create mode 100644 bindings/lua/tests/test_lpm.lua create mode 100644 docker/Dockerfile.lua diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88c03ae..914f80a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,6 +115,35 @@ jobs: run: | docker run --rm liblpm-go:ci + # Lua bindings test + test-lua-bindings: + name: Test Lua Bindings + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Lua container + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile.lua + push: false + load: true + tags: liblpm-lua:ci + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Run Lua tests + run: | + docker run --rm liblpm-lua:ci + # Code quality checks code-quality: name: Code Quality @@ -167,7 +196,7 @@ jobs: ci-summary: name: CI Summary runs-on: ubuntu-latest - needs: [build-and-test, test-cpp-bindings, test-go-bindings, code-quality] + needs: [build-and-test, test-cpp-bindings, test-go-bindings, test-lua-bindings, code-quality] if: always() steps: @@ -177,12 +206,14 @@ jobs: echo "Build and test: ${{ needs.build-and-test.result }}" echo "C++ bindings: ${{ needs.test-cpp-bindings.result }}" echo "Go bindings: ${{ needs.test-go-bindings.result }}" + echo "Lua bindings: ${{ needs.test-lua-bindings.result }}" echo "Code quality: ${{ needs.code-quality.result }}" # Fail if any required job failed if [[ "${{ needs.build-and-test.result }}" == "failure" ]] || \ [[ "${{ needs.test-cpp-bindings.result }}" == "failure" ]] || \ - [[ "${{ needs.test-go-bindings.result }}" == "failure" ]]; then + [[ "${{ needs.test-go-bindings.result }}" == "failure" ]] || \ + [[ "${{ needs.test-lua-bindings.result }}" == "failure" ]]; then echo "One or more required jobs failed" exit 1 fi diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c5b7da..d913e05 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,6 +61,7 @@ option(WITH_DPDK_BENCHMARK "Build DPDK comparison benchmark (requires DPDK)" OFF option(WITH_EXTERNAL_LPM_BENCHMARK "Build benchmarks with external LPM libraries (Docker only)" OFF) option(BUILD_GO_WRAPPER "Build Go wrapper and bindings" OFF) option(BUILD_CPP_WRAPPER "Build C++ wrapper and bindings" OFF) +option(BUILD_LUA_WRAPPER "Build Lua wrapper and bindings" OFF) # External LPM libraries directory (set by Docker builds) set(EXTERNAL_LPM_DIR "" CACHE PATH "Directory containing external LPM libraries") @@ -390,6 +391,11 @@ if(BUILD_GO_WRAPPER) endif() endif() +# Lua wrapper +if(BUILD_LUA_WRAPPER) + add_subdirectory(bindings/lua) +endif() + # Print configuration summary message(STATUS "liblpm configuration:") message(STATUS " Version: ${PROJECT_VERSION}") @@ -435,6 +441,11 @@ if(BUILD_CPP_WRAPPER) else() message(STATUS " Build C++ wrapper: OFF") endif() +if(BUILD_LUA_WRAPPER) + message(STATUS " Build Lua wrapper: ON") +else() + message(STATUS " Build Lua wrapper: OFF") +endif() # ============================================================================ # CPack Configuration for .deb and .rpm packages diff --git a/bindings/lua/CMakeLists.txt b/bindings/lua/CMakeLists.txt new file mode 100644 index 0000000..0195765 --- /dev/null +++ b/bindings/lua/CMakeLists.txt @@ -0,0 +1,208 @@ +# ============================================================================ +# CMakeLists.txt for liblpm Lua bindings +# ============================================================================ +# +# Supports: +# - Lua 5.3, 5.4, and LuaJIT 2.1+ +# - CMake 3.16+ +# +# Build options: +# cmake -DBUILD_LUA_WRAPPER=ON .. +# +# Targets: +# lpm_lua - Lua C module (shared library) +# lua_test - Run Lua tests +# lua_example - Run basic example +# +# ============================================================================ + +cmake_minimum_required(VERSION 3.16) + +# ============================================================================ +# Find Lua +# ============================================================================ + +# Try to find Lua using CMake's FindLua module +# This will search for Lua 5.4, 5.3, 5.2, 5.1 in order +find_package(Lua) + +if(NOT LUA_FOUND) + # Try pkg-config as fallback + find_package(PkgConfig QUIET) + if(PkgConfig_FOUND) + # Try specific versions + pkg_check_modules(LUA QUIET lua5.4) + if(NOT LUA_FOUND) + pkg_check_modules(LUA QUIET lua5.3) + endif() + if(NOT LUA_FOUND) + pkg_check_modules(LUA QUIET luajit) + endif() + if(NOT LUA_FOUND) + pkg_check_modules(LUA QUIET lua) + endif() + endif() +endif() + +if(NOT LUA_FOUND) + message(WARNING "Lua not found. Lua bindings will not be built.") + message(WARNING "Install Lua development files:") + message(WARNING " Ubuntu/Debian: sudo apt install liblua5.4-dev") + message(WARNING " Fedora: sudo dnf install lua-devel") + message(WARNING " macOS: brew install lua") + return() +endif() + +# Check Lua version (require 5.3+) +if(LUA_VERSION_STRING VERSION_LESS "5.3") + message(WARNING "Lua ${LUA_VERSION_STRING} found, but 5.3+ is required.") + message(WARNING "Lua bindings will not be built.") + return() +endif() + +message(STATUS "Found Lua: ${LUA_VERSION_STRING}") +message(STATUS " Include: ${LUA_INCLUDE_DIR}") +message(STATUS " Library: ${LUA_LIBRARIES}") + +# ============================================================================ +# Lua C Module +# ============================================================================ + +# Source files +set(LPM_LUA_SOURCES + src/liblpm.c + src/liblpm_utils.c +) + +# Create shared library module +add_library(lpm_lua MODULE ${LPM_LUA_SOURCES}) + +# Set output name to match Lua's require() expectations +# The module will be named "liblpm.so" (or .dll on Windows) +set_target_properties(lpm_lua PROPERTIES + OUTPUT_NAME "liblpm" + PREFIX "" + SUFFIX ".so" +) + +# Platform-specific suffix +if(APPLE) + set_target_properties(lpm_lua PROPERTIES SUFFIX ".so") +elseif(WIN32) + set_target_properties(lpm_lua PROPERTIES SUFFIX ".dll") +endif() + +# Include directories +target_include_directories(lpm_lua PRIVATE + ${LUA_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/include +) + +# Link against liblpm shared library +# Note: The liblpm.so must be loadable (in LD_LIBRARY_PATH or installed) +# before the Lua module is loaded, due to ifunc resolver requirements +target_link_libraries(lpm_lua PRIVATE + lpm + ${LUA_LIBRARIES} +) + +# Set RPATH to find liblpm.so in the build directory and install location +set_target_properties(lpm_lua PROPERTIES + BUILD_RPATH "${CMAKE_BINARY_DIR}" + INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}" +) + +# C standard +set_target_properties(lpm_lua PROPERTIES + C_STANDARD 11 + C_STANDARD_REQUIRED ON +) + +# Compiler flags +if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(lpm_lua PRIVATE + -Wall -Wextra -Wpedantic + -Wno-unused-parameter + ) +endif() + +# ============================================================================ +# Installation +# ============================================================================ + +# Determine Lua install path +if(DEFINED LUA_CPATH) + set(LUA_INSTALL_CPATH ${LUA_CPATH}) +else() + # Default to standard Lua cpath + # This can be overridden with -DLUA_CPATH=/path/to/lua/cpath + set(LUA_INSTALL_CPATH "${CMAKE_INSTALL_LIBDIR}/lua/${LUA_VERSION_MAJOR}.${LUA_VERSION_MINOR}") +endif() + +install(TARGETS lpm_lua + LIBRARY DESTINATION ${LUA_INSTALL_CPATH} + RUNTIME DESTINATION ${LUA_INSTALL_CPATH} +) + +# ============================================================================ +# Test Target +# ============================================================================ + +# Find Lua interpreter +find_program(LUA_EXECUTABLE + NAMES lua${LUA_VERSION_MAJOR}.${LUA_VERSION_MINOR} lua + HINTS ${LUA_INCLUDE_DIR}/../bin +) + +if(LUA_EXECUTABLE) + message(STATUS "Lua interpreter: ${LUA_EXECUTABLE}") + + # Test target + # Note: LD_PRELOAD is required to ensure liblpm.so's ifunc resolvers are + # executed before the Lua module is loaded via dlopen + add_custom_target(lua_test + COMMAND ${CMAKE_COMMAND} -E env + "LD_PRELOAD=${CMAKE_BINARY_DIR}/liblpm.so" + "LUA_CPATH=${CMAKE_CURRENT_BINARY_DIR}/?.so" + ${LUA_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_lpm.lua + DEPENDS lpm_lua lpm + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Running Lua tests" + ) + + # Example target + add_custom_target(lua_example + COMMAND ${CMAKE_COMMAND} -E env + "LD_PRELOAD=${CMAKE_BINARY_DIR}/liblpm.so" + "LUA_CPATH=${CMAKE_CURRENT_BINARY_DIR}/?.so" + ${LUA_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/examples/basic_example.lua + DEPENDS lpm_lua lpm + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Running Lua basic example" + ) + + # Add to CTest + if(BUILD_TESTS) + add_test(NAME lua_wrapper_test + COMMAND ${CMAKE_COMMAND} -E env + "LD_PRELOAD=${CMAKE_BINARY_DIR}/liblpm.so" + "LUA_CPATH=${CMAKE_CURRENT_BINARY_DIR}/?.so" + ${LUA_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_lpm.lua + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endif() +else() + message(WARNING "Lua interpreter not found. Test targets will not be available.") +endif() + +# ============================================================================ +# Summary +# ============================================================================ + +message(STATUS "Lua bindings configuration:") +message(STATUS " Lua version: ${LUA_VERSION_STRING}") +message(STATUS " Install path: ${LUA_INSTALL_CPATH}") +if(LUA_EXECUTABLE) + message(STATUS " Test command: make lua_test") + message(STATUS " Example command: make lua_example") +endif() diff --git a/bindings/lua/Makefile b/bindings/lua/Makefile new file mode 100644 index 0000000..134cb9f --- /dev/null +++ b/bindings/lua/Makefile @@ -0,0 +1,113 @@ +# ============================================================================ +# Makefile for liblpm Lua bindings (standalone build) +# ============================================================================ +# +# Usage: +# make - Build the Lua module +# make test - Run tests +# make example - Run basic example +# make clean - Remove build artifacts +# make install - Install to Lua cpath +# +# Configuration (override with environment variables or make arguments): +# LUA=lua5.4 - Lua interpreter +# LUA_CFLAGS=... - Compiler flags for Lua headers +# LUA_LIBS=... - Linker flags for Lua +# LPM_CFLAGS=... - Compiler flags for liblpm +# LPM_LIBS=... - Linker flags for liblpm +# PREFIX=/usr/local - Installation prefix +# +# ============================================================================ + +# Lua configuration (auto-detect if not specified) +LUA ?= lua +LUA_CONFIG ?= $(shell which lua-config 2>/dev/null || which pkg-config 2>/dev/null) + +# Try to detect Lua version and flags +ifeq ($(LUA_CFLAGS),) + LUA_CFLAGS := $(shell pkg-config --cflags lua5.4 2>/dev/null || \ + pkg-config --cflags lua5.3 2>/dev/null || \ + pkg-config --cflags luajit 2>/dev/null || \ + pkg-config --cflags lua 2>/dev/null || \ + echo "-I/usr/include/lua5.4") +endif + +ifeq ($(LUA_LIBS),) + LUA_LIBS := $(shell pkg-config --libs lua5.4 2>/dev/null || \ + pkg-config --libs lua5.3 2>/dev/null || \ + pkg-config --libs luajit 2>/dev/null || \ + pkg-config --libs lua 2>/dev/null || \ + echo "-llua5.4") +endif + +# liblpm configuration +ifeq ($(LPM_CFLAGS),) + LPM_CFLAGS := $(shell pkg-config --cflags liblpm 2>/dev/null || echo "-I/usr/local/include/lpm") +endif + +ifeq ($(LPM_LIBS),) + LPM_LIBS := $(shell pkg-config --libs liblpm 2>/dev/null || echo "-L/usr/local/lib -llpm") +endif + +# Installation +PREFIX ?= /usr/local +LUA_CPATH ?= $(shell $(LUA) -e "print(package.cpath:match('([^;]+)'))" 2>/dev/null | sed 's|/[^/]*$$||' || echo "$(PREFIX)/lib/lua/5.4") + +# Compiler settings +CC ?= gcc +CFLAGS ?= -Wall -Wextra -Wpedantic -O2 -fPIC +LDFLAGS ?= -shared + +# Source files +SOURCES = src/liblpm.c src/liblpm_utils.c +OBJECTS = $(SOURCES:.c=.o) +TARGET = liblpm.so + +# ============================================================================ +# Targets +# ============================================================================ + +.PHONY: all clean test example install uninstall info + +all: $(TARGET) + +$(TARGET): $(OBJECTS) + $(CC) $(LDFLAGS) -o $@ $^ $(LPM_LIBS) $(LUA_LIBS) + +src/%.o: src/%.c + $(CC) $(CFLAGS) $(LUA_CFLAGS) $(LPM_CFLAGS) -c -o $@ $< + +clean: + rm -f $(OBJECTS) $(TARGET) + +test: $(TARGET) + @echo "Running Lua tests..." + @echo "Note: LD_PRELOAD is required due to ifunc resolvers in liblpm" + LD_PRELOAD=/usr/local/lib/liblpm.so LUA_CPATH="./?.so" $(LUA) tests/test_lpm.lua + +example: $(TARGET) + @echo "Running basic example..." + @echo "Note: LD_PRELOAD is required due to ifunc resolvers in liblpm" + LD_PRELOAD=/usr/local/lib/liblpm.so LUA_CPATH="./?.so" $(LUA) examples/basic_example.lua + +install: $(TARGET) + @echo "Installing to $(LUA_CPATH)..." + install -d $(LUA_CPATH) + install -m 644 $(TARGET) $(LUA_CPATH)/$(TARGET) + +uninstall: + rm -f $(LUA_CPATH)/$(TARGET) + +info: + @echo "Lua bindings configuration:" + @echo " Lua: $(LUA)" + @echo " LUA_CFLAGS: $(LUA_CFLAGS)" + @echo " LUA_LIBS: $(LUA_LIBS)" + @echo " LPM_CFLAGS: $(LPM_CFLAGS)" + @echo " LPM_LIBS: $(LPM_LIBS)" + @echo " LUA_CPATH: $(LUA_CPATH)" + @echo " Target: $(TARGET)" + +# Dependencies +src/liblpm.o: src/liblpm.c +src/liblpm_utils.o: src/liblpm_utils.c diff --git a/bindings/lua/README.md b/bindings/lua/README.md new file mode 100644 index 0000000..5ebee2a --- /dev/null +++ b/bindings/lua/README.md @@ -0,0 +1,437 @@ +# Lua Bindings for liblpm + +High-performance Lua bindings for the [liblpm](https://github.com/MuriloChianfa/liblpm) C library, providing fast longest prefix match (LPM) routing table operations for both IPv4 and IPv6. + +## Features + +- **High Performance**: Direct C bindings with minimal overhead +- **IPv4 DIR-24-8**: Optimized IPv4 lookups with 1-2 memory accesses +- **IPv6 Wide Stride**: Efficient IPv6 lookups with 16-bit first-level stride +- **Batch Operations**: Process multiple addresses in a single call +- **Multiple Input Formats**: CIDR strings, dotted-decimal, colon-hex, byte tables +- **Dual API Style**: Object-oriented and functional interfaces +- **Automatic Cleanup**: Garbage collector integration with optional explicit close +- **Lua 5.3+**: Supports Lua 5.3, 5.4, and LuaJIT 2.1+ + +## Installation + +### Prerequisites + +First, ensure liblpm is installed: + +```bash +# Build and install liblpm +cd /path/to/liblpm +mkdir -p build && cd build +cmake .. +make -j$(nproc) +sudo make install +sudo ldconfig +``` + +### From Source (CMake) + +```bash +# Build with Lua bindings +cd /path/to/liblpm +mkdir -p build && cd build +cmake -DBUILD_LUA_WRAPPER=ON .. +make -j$(nproc) + +# Run tests +make lua_test + +# Install (optional) +sudo make install +``` + +### LuaRocks + +```bash +# Install from local rockspec +cd bindings/lua +luarocks make liblpm-2.0.0-1.rockspec + +# Or install directly (once published) +luarocks install liblpm +``` + +### Manual Compilation + +```bash +# Compile the module directly +cd bindings/lua +gcc -shared -fPIC -O2 \ + -I/usr/include/lua5.4 \ + -I../../include \ + src/liblpm.c src/liblpm_utils.c \ + -llpm -llua5.4 \ + -o liblpm.so +``` + +## Important Note + +Due to the use of GNU ifunc for SIMD runtime dispatch in liblpm, when loading +the Lua module via `require()`, you may need to preload `liblpm.so`: + +```bash +# If liblpm is installed system-wide +lua your_script.lua + +# If using a local build, preload the library +LD_PRELOAD=/path/to/liblpm.so lua your_script.lua +``` + +This ensures the ifunc resolvers are executed before the Lua module is loaded. +The CMake build system handles this automatically for tests and examples. + +## Quick Start + +```lua +local lpm = require("liblpm") + +-- Create IPv4 routing table +local table = lpm.new_ipv4() + +-- Insert routes +table:insert("10.0.0.0/8", 100) +table:insert("192.168.0.0/16", 200) +table:insert("0.0.0.0/0", 999) -- Default route + +-- Lookup addresses +local next_hop = table:lookup("192.168.1.1") +print("Next hop:", next_hop) -- 200 + +-- Batch lookup +local results = table:lookup_batch({"10.1.1.1", "192.168.2.1", "8.8.8.8"}) +-- results = {100, 200, 999} + +-- Clean up +table:close() +``` + +## API Reference + +### Module Functions + +#### Table Creation + +```lua +-- Create IPv4 table with algorithm selection +local table = lpm.new_ipv4([algorithm]) +-- algorithm: "dir24" (default) or "stride8" + +-- Create IPv6 table with algorithm selection +local table = lpm.new_ipv6([algorithm]) +-- algorithm: "wide16" (default) or "stride8" +``` + +#### Utility Functions + +```lua +-- Get library version +local version = lpm.version() -- "liblpm 2.0.0" + +-- Get Lua binding version +local lua_version = lpm.lua_version() -- "2.0.0" +``` + +#### Constants + +```lua +lpm.INVALID_NEXT_HOP -- 0xFFFFFFFF (returned when no match) +lpm.IPV4_MAX_DEPTH -- 32 +lpm.IPV6_MAX_DEPTH -- 128 +lpm._VERSION -- "2.0.0" +``` + +### Table Methods (Object-Oriented Style) + +#### insert + +```lua +-- CIDR notation +local ok, err = table:insert("192.168.0.0/16", next_hop) + +-- Address + prefix length +local ok, err = table:insert("192.168.0.0", 16, next_hop) + +-- Byte table + prefix length +local ok, err = table:insert({192, 168, 0, 0}, 16, next_hop) +``` + +Returns `true` on success, or `nil, error_message` on failure. + +#### delete + +```lua +-- CIDR notation +local ok, err = table:delete("192.168.0.0/16") + +-- Address + prefix length +local ok, err = table:delete("192.168.0.0", 16) +``` + +Returns `true` on success, or `nil, error_message` on failure. + +#### lookup + +```lua +-- String address +local next_hop = table:lookup("192.168.1.1") + +-- Byte table +local next_hop = table:lookup({192, 168, 1, 1}) + +-- Binary string (4 bytes for IPv4, 16 for IPv6) +local next_hop = table:lookup(binary_addr) +``` + +Returns `next_hop` integer on match, or `nil` if no match found. + +#### lookup_batch + +```lua +local results = table:lookup_batch(addresses) +``` + +- `addresses`: Table of IP addresses (any supported format) +- Returns: Table where `results[i]` is `next_hop` or `nil` for `addresses[i]` + +#### close + +```lua +table:close() +``` + +Explicitly release resources. Safe to call multiple times. + +#### is_closed + +```lua +local closed = table:is_closed() -- boolean +``` + +#### is_ipv6 + +```lua +local ipv6 = table:is_ipv6() -- boolean +``` + +### Functional API Style + +All table methods are also available as module functions: + +```lua +lpm.insert(table, prefix, next_hop) +lpm.delete(table, prefix) +lpm.lookup(table, address) +lpm.lookup_batch(table, addresses) +lpm.close(table) +lpm.is_closed(table) +``` + +## Address Formats + +### IPv4 + +| Format | Example | Description | +|--------|---------|-------------| +| CIDR string | `"192.168.0.0/16"` | Most common format | +| Dotted-decimal | `"192.168.1.1"` | For lookups or with prefix_len | +| Byte table | `{192, 168, 1, 1}` | Zero parsing overhead | +| Binary string | `"\xC0\xA8\x01\x01"` | 4-byte string | + +### IPv6 + +| Format | Example | Description | +|--------|---------|-------------| +| CIDR string | `"2001:db8::/32"` | Most common format | +| Colon-hex | `"2001:db8::1"` | For lookups or with prefix_len | +| Compressed | `"::1"`, `"fe80::"` | Standard IPv6 compression | +| Byte table | `{0x20, 0x01, ...}` | 16 bytes, zero parsing | +| Binary string | 16-byte string | Zero parsing overhead | + +## Algorithm Selection + +### IPv4 Algorithms + +| Algorithm | Description | Use Case | +|-----------|-------------|----------| +| `dir24` | DIR-24-8 with 24-bit direct table | Default, fastest for most workloads | +| `stride8` | 8-bit stride trie (4 levels) | Memory-constrained environments | + +### IPv6 Algorithms + +| Algorithm | Description | Use Case | +|-----------|-------------|----------| +| `wide16` | 16-bit first stride, then 8-bit | Default, optimized for /48 allocations | +| `stride8` | 8-bit stride trie (16 levels) | Simpler, may use less memory | + +## Error Handling + +```lua +-- Insert returns boolean success and optional error +local ok, err = table:insert("invalid/prefix", 100) +if not ok then + print("Error:", err) +end + +-- Lookup returns nil for no match +local nh = table:lookup("8.8.8.8") +if nh then + print("Found:", nh) +else + print("No match") +end + +-- Operations on closed tables raise errors +local ok, err = pcall(function() + closed_table:lookup("1.1.1.1") +end) +-- ok = false, err contains error message +``` + +## Memory Management + +The bindings use Lua's garbage collector with a `__gc` metamethod to automatically clean up resources. However, for deterministic cleanup, call `close()` explicitly: + +```lua +local table = lpm.new_ipv4() +-- ... use table ... +table:close() -- Immediate cleanup + +-- Or let GC handle it (not recommended for long-lived scripts) +table = nil +collectgarbage() +``` + +### Best Practices + +1. **Explicit close**: Always call `close()` when done +2. **defer pattern**: Use `pcall` or similar for cleanup on error +3. **Reuse tables**: Avoid frequent create/destroy cycles + +## Performance Tips + +### Use Batch Operations + +```lua +-- Better: Single batch call +local results = table:lookup_batch(addresses) + +-- Worse: Individual calls +for _, addr in ipairs(addresses) do + local nh = table:lookup(addr) +end +``` + +### Use Byte Format for Hot Paths + +```lua +-- Fastest: Byte table (no parsing) +local nh = table:lookup({192, 168, 1, 1}) + +-- Fast: Binary string (no parsing) +local nh = table:lookup("\xC0\xA8\x01\x01") + +-- Slower: String (requires parsing) +local nh = table:lookup("192.168.1.1") +``` + +### Keep Tables Open + +```lua +-- Better: Reuse table +local router = lpm.new_ipv4() +-- Insert routes once +for _ = 1, 1000000 do + router:lookup(...) +end +router:close() + +-- Worse: Create/destroy repeatedly +for _ = 1, 1000000 do + local t = lpm.new_ipv4() + t:insert(...) + t:lookup(...) + t:close() +end +``` + +## Thread Safety + +The Lua bindings are **not thread-safe**. If using with coroutines or multiple Lua states: + +- Each Lua state should have its own table instances +- Do not share table userdata between states +- Use external locking if concurrent access is required + +## Examples + +See the [examples](examples/) directory: + +- [basic_example.lua](examples/basic_example.lua) - Getting started +- [ipv6_example.lua](examples/ipv6_example.lua) - IPv6 operations +- [batch_example.lua](examples/batch_example.lua) - Batch processing + +Run examples: + +```bash +# Via CMake +make lua_example + +# Directly +lua examples/basic_example.lua +``` + +## Testing + +```bash +# Run test suite +make lua_test + +# Or directly +lua tests/test_lpm.lua + +# With verbose output +LUA_CPATH="./?.so;;" lua tests/test_lpm.lua +``` + +## Requirements + +- Lua 5.3 or later (5.4 recommended) +- LuaJIT 2.1+ (alternative) +- liblpm 2.0.0 or later +- GCC or Clang +- Linux or macOS + +## Limitations + +- Not thread-safe (use external locking for concurrent access) +- IPv4 and IPv6 require separate table instances +- Maximum batch size: 100,000 addresses + +## Contributing + +Contributions welcome! Please ensure: + +- All tests pass: `make lua_test` +- Code follows existing style +- New features include tests and documentation + +## License + +Same as liblpm: [Boost Software License 1.0](../../LICENSE) + +## Credits + +- [liblpm](https://github.com/MuriloChianfa/liblpm) by Murilo Chianfa +- Lua bindings by Murilo Chianfa + +## See Also + +- [liblpm main documentation](../../README.md) +- [C++ bindings](../cpp/README.md) +- [Go bindings](../go/README.md) +- [C API reference](../../include/lpm.h) diff --git a/bindings/lua/examples/basic_example.lua b/bindings/lua/examples/basic_example.lua new file mode 100644 index 0000000..ef0b408 --- /dev/null +++ b/bindings/lua/examples/basic_example.lua @@ -0,0 +1,208 @@ +#!/usr/bin/env lua +-- ============================================================================ +-- basic_example.lua - Basic usage example for liblpm Lua bindings +-- ============================================================================ +-- +-- This example demonstrates: +-- - Creating IPv4 routing tables +-- - Inserting routes using different formats +-- - Looking up addresses +-- - Proper resource cleanup +-- +-- Run: lua examples/basic_example.lua +-- Or via CMake: make lua_example +-- +-- ============================================================================ + +local lpm = require("liblpm") + +-- Print header +print("=" .. string.rep("=", 60)) +print("liblpm Lua bindings - Basic Example") +print("=" .. string.rep("=", 60)) +print(string.format("Library version: %s", lpm.version())) +print(string.format("Lua binding version: %s", lpm._VERSION)) +print("") + +-- ============================================================================ +-- Example 1: Basic IPv4 Routing Table +-- ============================================================================ + +print("Example 1: Basic IPv4 Routing Table") +print("-" .. string.rep("-", 40)) + +-- Create an IPv4 routing table using the default (dir24) algorithm +local ipv4_table = lpm.new_ipv4() + +-- Define routes with descriptive names +local routes = { + -- CIDR notation: prefix/length, next_hop + {"10.0.0.0/8", 100, "Private Class A"}, + {"172.16.0.0/12", 200, "Private Class B"}, + {"192.168.0.0/16", 300, "Private Class C"}, + {"192.168.1.0/24", 301, "LAN Subnet 1"}, + {"192.168.2.0/24", 302, "LAN Subnet 2"}, + {"0.0.0.0/0", 999, "Default Route"}, +} + +-- Insert routes +print("\nInserting routes:") +for _, route in ipairs(routes) do + local prefix, next_hop, name = route[1], route[2], route[3] + local ok, err = ipv4_table:insert(prefix, next_hop) + if ok then + print(string.format(" %-20s -> %3d (%s)", prefix, next_hop, name)) + else + print(string.format(" %-20s FAILED: %s", prefix, err)) + end +end + +-- Lookup some addresses +print("\nLooking up addresses:") +local test_addrs = { + "10.1.2.3", -- Should match 10.0.0.0/8 -> 100 + "172.20.1.1", -- Should match 172.16.0.0/12 -> 200 + "192.168.1.100", -- Should match 192.168.1.0/24 -> 301 (most specific) + "192.168.2.50", -- Should match 192.168.2.0/24 -> 302 + "192.168.3.1", -- Should match 192.168.0.0/16 -> 300 + "8.8.8.8", -- Should match 0.0.0.0/0 -> 999 (default) +} + +for _, addr in ipairs(test_addrs) do + local next_hop = ipv4_table:lookup(addr) + if next_hop then + print(string.format(" %-16s -> next_hop = %d", addr, next_hop)) + else + print(string.format(" %-16s -> no match", addr)) + end +end + +-- Clean up +ipv4_table:close() +print("\nTable closed.") + +-- ============================================================================ +-- Example 2: Different Input Formats +-- ============================================================================ + +print("\n" .. "=" .. string.rep("=", 60)) +print("Example 2: Different Input Formats") +print("-" .. string.rep("-", 40)) + +local table2 = lpm.new_ipv4() + +-- Format 1: CIDR string +print("\nFormat 1: CIDR string") +table2:insert("10.0.0.0/8", 1) +print(" table2:insert(\"10.0.0.0/8\", 1)") + +-- Format 2: Address string + prefix length +print("\nFormat 2: Address + prefix length") +table2:insert("172.16.0.0", 12, 2) +print(" table2:insert(\"172.16.0.0\", 12, 2)") + +-- Format 3: Byte table + prefix length +print("\nFormat 3: Byte table + prefix length") +table2:insert({192, 168, 0, 0}, 16, 3) +print(" table2:insert({192, 168, 0, 0}, 16, 3)") + +-- Verify all formats work +print("\nVerification:") +print(string.format(" 10.1.1.1 -> %d (expected: 1)", table2:lookup("10.1.1.1"))) +print(string.format(" 172.20.1.1 -> %d (expected: 2)", table2:lookup("172.20.1.1"))) +print(string.format(" 192.168.1.1 -> %d (expected: 3)", table2:lookup("192.168.1.1"))) + +-- Lookup with byte table +local nh = table2:lookup({10, 2, 3, 4}) +print(string.format(" {10,2,3,4} -> %d (expected: 1)", nh)) + +table2:close() + +-- ============================================================================ +-- Example 3: Route Deletion +-- ============================================================================ + +print("\n" .. "=" .. string.rep("=", 60)) +print("Example 3: Route Deletion") +print("-" .. string.rep("-", 40)) + +local table3 = lpm.new_ipv4() + +-- Insert routes +table3:insert("192.168.0.0/16", 100) +table3:insert("192.168.1.0/24", 200) + +print("\nBefore deletion:") +print(string.format(" 192.168.1.1 -> %d (matches /24)", table3:lookup("192.168.1.1"))) +print(string.format(" 192.168.2.1 -> %d (matches /16)", table3:lookup("192.168.2.1"))) + +-- Delete the more specific route +print("\nDeleting 192.168.1.0/24...") +table3:delete("192.168.1.0/24") + +-- Note: DIR-24-8 algorithm delete clears entries without re-applying +-- shorter prefixes. For proper fallback behavior, use stride8 algorithm +-- or re-insert the shorter prefix after deletion. +print("\nAfter deletion:") +local nh1 = table3:lookup("192.168.1.1") +local nh2 = table3:lookup("192.168.2.1") +print(string.format(" 192.168.1.1 -> %s", nh1 and tostring(nh1) or "nil")) +print(string.format(" 192.168.2.1 -> %s (still matches /16)", nh2 and tostring(nh2) or "nil")) + +table3:close() + +-- ============================================================================ +-- Example 4: Functional API Style +-- ============================================================================ + +print("\n" .. "=" .. string.rep("=", 60)) +print("Example 4: Functional API Style") +print("-" .. string.rep("-", 40)) + +local table4 = lpm.new_ipv4() + +-- Using functional style (lpm.insert instead of table:insert) +lpm.insert(table4, "10.0.0.0/8", 100) +lpm.insert(table4, "192.168.0.0/16", 200) + +-- Functional lookup +local nh1 = lpm.lookup(table4, "10.1.1.1") +local nh2 = lpm.lookup(table4, "192.168.1.1") + +print(string.format("\n lpm.lookup(table, \"10.1.1.1\") -> %d", nh1)) +print(string.format(" lpm.lookup(table, \"192.168.1.1\") -> %d", nh2)) + +-- Functional close +print(string.format("\n lpm.is_closed(table) -> %s", tostring(lpm.is_closed(table4)))) +lpm.close(table4) +print(string.format(" lpm.is_closed(table) -> %s (after close)", tostring(lpm.is_closed(table4)))) + +-- ============================================================================ +-- Example 5: Algorithm Selection +-- ============================================================================ + +print("\n" .. "=" .. string.rep("=", 60)) +print("Example 5: Algorithm Selection") +print("-" .. string.rep("-", 40)) + +-- DIR-24-8 algorithm (default, optimal for most IPv4 use cases) +print("\nDIR-24-8 algorithm (default):") +local dir24_table = lpm.new_ipv4("dir24") +dir24_table:insert("10.0.0.0/8", 100) +print(string.format(" 10.1.1.1 -> %d", dir24_table:lookup("10.1.1.1"))) +dir24_table:close() + +-- 8-bit stride algorithm +print("\n8-bit stride algorithm:") +local stride8_table = lpm.new_ipv4("stride8") +stride8_table:insert("10.0.0.0/8", 100) +print(string.format(" 10.1.1.1 -> %d", stride8_table:lookup("10.1.1.1"))) +stride8_table:close() + +-- ============================================================================ +-- Summary +-- ============================================================================ + +print("\n" .. "=" .. string.rep("=", 60)) +print("Example complete!") +print("=" .. string.rep("=", 60)) diff --git a/bindings/lua/examples/batch_example.lua b/bindings/lua/examples/batch_example.lua new file mode 100644 index 0000000..27c2810 --- /dev/null +++ b/bindings/lua/examples/batch_example.lua @@ -0,0 +1,336 @@ +#!/usr/bin/env lua +-- ============================================================================ +-- batch_example.lua - Batch operations example for liblpm Lua bindings +-- ============================================================================ +-- +-- This example demonstrates: +-- - Batch lookup operations for high throughput +-- - Processing large numbers of addresses efficiently +-- - Performance comparison between single and batch lookups +-- +-- Run: lua examples/batch_example.lua +-- +-- ============================================================================ + +local lpm = require("liblpm") + +-- Print header +print("=" .. string.rep("=", 60)) +print("liblpm Lua bindings - Batch Operations Example") +print("=" .. string.rep("=", 60)) +print("") + +-- ============================================================================ +-- Example 1: Basic Batch Lookup +-- ============================================================================ + +print("Example 1: Basic Batch Lookup") +print("-" .. string.rep("-", 40)) + +local table1 = lpm.new_ipv4() + +-- Insert routes +table1:insert("10.0.0.0/8", 100) +table1:insert("172.16.0.0/12", 200) +table1:insert("192.168.0.0/16", 300) +table1:insert("0.0.0.0/0", 999) + +print("\nRoutes inserted:") +print(" 10.0.0.0/8 -> 100 (Private Class A)") +print(" 172.16.0.0/12 -> 200 (Private Class B)") +print(" 192.168.0.0/16 -> 300 (Private Class C)") +print(" 0.0.0.0/0 -> 999 (Default)") + +-- Prepare batch of addresses +local addresses = { + "10.1.1.1", + "10.2.2.2", + "172.16.1.1", + "172.20.1.1", + "192.168.1.1", + "192.168.100.50", + "8.8.8.8", + "1.1.1.1", +} + +print(string.format("\nBatch lookup of %d addresses:", #addresses)) + +-- Perform batch lookup +local results = table1:lookup_batch(addresses) + +-- Display results +print(string.format("\n %-16s | %s", "Address", "Next Hop")) +print(" " .. string.rep("-", 16) .. " | " .. string.rep("-", 10)) +for i, addr in ipairs(addresses) do + local nh = results[i] + if nh then + print(string.format(" %-16s | %d", addr, nh)) + else + print(string.format(" %-16s | (no match)", addr)) + end +end + +table1:close() + +-- ============================================================================ +-- Example 2: Large Batch Processing +-- ============================================================================ + +print("\n" .. "=" .. string.rep("=", 60)) +print("Example 2: Large Batch Processing") +print("-" .. string.rep("-", 40)) + +local table2 = lpm.new_ipv4() + +-- Insert a variety of routes +print("\nBuilding routing table...") +local route_count = 0 + +-- Add some /8 routes +for i = 1, 10 do + table2:insert(string.format("%d.0.0.0/8", i), i) + route_count = route_count + 1 +end + +-- Add some /16 routes +for i = 11, 50 do + table2:insert(string.format("10.%d.0.0/16", i), i) + route_count = route_count + 1 +end + +-- Add some /24 routes +for i = 1, 100 do + table2:insert(string.format("192.168.%d.0/24", i), 1000 + i) + route_count = route_count + 1 +end + +-- Default route +table2:insert("0.0.0.0/0", 9999) +route_count = route_count + 1 + +print(string.format(" Inserted %d routes", route_count)) + +-- Generate test addresses +local function generate_random_ip() + return string.format("%d.%d.%d.%d", + math.random(0, 255), + math.random(0, 255), + math.random(0, 255), + math.random(0, 255)) +end + +-- Set random seed for reproducibility +math.randomseed(12345) + +local batch_size = 10000 +print(string.format("\nGenerating %d random addresses...", batch_size)) + +local large_batch = {} +for i = 1, batch_size do + large_batch[i] = generate_random_ip() +end + +-- Time the batch lookup +print("Performing batch lookup...") +local start_time = os.clock() +local batch_results = table2:lookup_batch(large_batch) +local batch_time = os.clock() - start_time + +print(string.format("\nBatch lookup completed:")) +print(string.format(" Addresses: %d", batch_size)) +print(string.format(" Time: %.4f seconds", batch_time)) +print(string.format(" Rate: %.0f lookups/sec", batch_size / batch_time)) + +-- Count matches +local match_count = 0 +for i = 1, batch_size do + if batch_results[i] then + match_count = match_count + 1 + end +end +print(string.format(" Matches: %d (%.1f%%)", match_count, 100 * match_count / batch_size)) + +table2:close() + +-- ============================================================================ +-- Example 3: IPv6 Batch Lookup +-- ============================================================================ + +print("\n" .. "=" .. string.rep("=", 60)) +print("Example 3: IPv6 Batch Lookup") +print("-" .. string.rep("-", 40)) + +local table3 = lpm.new_ipv6() + +-- Insert IPv6 routes +table3:insert("2001:db8::/32", 100) +table3:insert("2001:db8:1::/48", 200) +table3:insert("fe80::/10", 300) +table3:insert("::/0", 999) + +print("\nIPv6 routes:") +print(" 2001:db8::/32 -> 100") +print(" 2001:db8:1::/48 -> 200") +print(" fe80::/10 -> 300") +print(" ::/0 -> 999") + +-- IPv6 batch +local ipv6_batch = { + "2001:db8::1", + "2001:db8:1::1", + "2001:db8:2::1", + "fe80::1", + "fe80::abcd:1234", + "2607:f8b0:4004::1", -- Google IPv6 + "::1", +} + +print(string.format("\nBatch lookup of %d IPv6 addresses:", #ipv6_batch)) + +local ipv6_results = table3:lookup_batch(ipv6_batch) + +print(string.format("\n %-24s | %s", "Address", "Next Hop")) +print(" " .. string.rep("-", 24) .. " | " .. string.rep("-", 10)) +for i, addr in ipairs(ipv6_batch) do + local nh = ipv6_results[i] + if nh then + print(string.format(" %-24s | %d", addr, nh)) + else + print(string.format(" %-24s | (no match)", addr)) + end +end + +table3:close() + +-- ============================================================================ +-- Example 4: Batch with Mixed Formats +-- ============================================================================ + +print("\n" .. "=" .. string.rep("=", 60)) +print("Example 4: Batch with Mixed Formats") +print("-" .. string.rep("-", 40)) + +local table4 = lpm.new_ipv4() +table4:insert("10.0.0.0/8", 100) +table4:insert("192.168.0.0/16", 200) + +print("\nAddresses in different formats:") + +-- Mix of formats in the batch +local mixed_batch = { + "10.1.1.1", -- String format + "10.2.2.2", -- String format + {192, 168, 1, 1}, -- Byte table format + {192, 168, 2, 2}, -- Byte table format + string.char(10, 3, 3, 3), -- Binary string format +} + +-- Note: All formats work in the same batch +local mixed_results = table4:lookup_batch(mixed_batch) + +print(" \"10.1.1.1\" -> " .. tostring(mixed_results[1])) +print(" \"10.2.2.2\" -> " .. tostring(mixed_results[2])) +print(" {192, 168, 1, 1} -> " .. tostring(mixed_results[3])) +print(" {192, 168, 2, 2} -> " .. tostring(mixed_results[4])) +print(" (binary 10.3.3.3) -> " .. tostring(mixed_results[5])) + +table4:close() + +-- ============================================================================ +-- Example 5: Packet Processing Simulation +-- ============================================================================ + +print("\n" .. "=" .. string.rep("=", 60)) +print("Example 5: Packet Processing Simulation") +print("-" .. string.rep("-", 40)) + +local router = lpm.new_ipv4() + +-- Simulate a router's routing table +print("\nSetting up router...") +router:insert("0.0.0.0/0", 1) -- Default -> Interface 1 +router:insert("10.0.0.0/8", 2) -- Internal -> Interface 2 +router:insert("172.16.0.0/12", 3) -- VPN -> Interface 3 +router:insert("192.168.1.0/24", 4) -- LAN1 -> Interface 4 +router:insert("192.168.2.0/24", 5) -- LAN2 -> Interface 5 + +-- Simulate incoming packet burst +local function generate_packet_burst(count) + local packets = {} + for i = 1, count do + -- Simulate realistic traffic distribution + local r = math.random(100) + if r <= 40 then + -- 40% internal traffic + packets[i] = string.format("10.%d.%d.%d", + math.random(0, 255), math.random(0, 255), math.random(0, 255)) + elseif r <= 60 then + -- 20% LAN traffic + packets[i] = string.format("192.168.%d.%d", + math.random(1, 2), math.random(1, 254)) + elseif r <= 70 then + -- 10% VPN traffic + packets[i] = string.format("172.%d.%d.%d", + math.random(16, 31), math.random(0, 255), math.random(0, 255)) + else + -- 30% external traffic (default route) + packets[i] = string.format("%d.%d.%d.%d", + math.random(11, 223), math.random(0, 255), + math.random(0, 255), math.random(0, 255)) + end + end + return packets +end + +-- Process multiple bursts +local total_packets = 0 +local total_time = 0 +local interface_counts = {0, 0, 0, 0, 0} + +print("\nProcessing packet bursts:") +for burst = 1, 5 do + local packets = generate_packet_burst(5000) + local start = os.clock() + local decisions = router:lookup_batch(packets) + local elapsed = os.clock() - start + + total_packets = total_packets + #packets + total_time = total_time + elapsed + + -- Count interface assignments + for _, iface in ipairs(decisions) do + if iface and iface >= 1 and iface <= 5 then + interface_counts[iface] = interface_counts[iface] + 1 + end + end + + print(string.format(" Burst %d: %d packets in %.4fs (%.0f pps)", + burst, #packets, elapsed, #packets / elapsed)) +end + +print(string.format("\nTotal: %d packets in %.4fs (%.0f pps average)", + total_packets, total_time, total_packets / total_time)) + +print("\nTraffic distribution by interface:") +local iface_names = {"Default", "Internal", "VPN", "LAN1", "LAN2"} +for i = 1, 5 do + print(string.format(" Interface %d (%s): %d packets (%.1f%%)", + i, iface_names[i], interface_counts[i], + 100 * interface_counts[i] / total_packets)) +end + +router:close() + +-- ============================================================================ +-- Summary +-- ============================================================================ + +print("\n" .. "=" .. string.rep("=", 60)) +print("Batch Operations Example complete!") +print("") +print("Key takeaways:") +print(" - Batch lookups are more efficient for multiple addresses") +print(" - Use lookup_batch() when processing packet bursts") +print(" - Mixed address formats work in the same batch") +print(" - IPv6 batch operations work the same way as IPv4") +print("=" .. string.rep("=", 60)) diff --git a/bindings/lua/examples/ipv6_example.lua b/bindings/lua/examples/ipv6_example.lua new file mode 100644 index 0000000..b90721c --- /dev/null +++ b/bindings/lua/examples/ipv6_example.lua @@ -0,0 +1,231 @@ +#!/usr/bin/env lua +-- ============================================================================ +-- ipv6_example.lua - IPv6 usage example for liblpm Lua bindings +-- ============================================================================ +-- +-- This example demonstrates: +-- - Creating IPv6 routing tables +-- - Inserting IPv6 routes with various notation styles +-- - IPv6 address lookups +-- - Algorithm selection for IPv6 +-- +-- Run: lua examples/ipv6_example.lua +-- +-- ============================================================================ + +local lpm = require("liblpm") + +-- Print header +print("=" .. string.rep("=", 60)) +print("liblpm Lua bindings - IPv6 Example") +print("=" .. string.rep("=", 60)) +print("") + +-- ============================================================================ +-- Example 1: Basic IPv6 Routing +-- ============================================================================ + +print("Example 1: Basic IPv6 Routing") +print("-" .. string.rep("-", 40)) + +-- Create IPv6 table with default (wide16) algorithm +local ipv6_table = lpm.new_ipv6() + +-- Common IPv6 prefixes and their uses +local routes = { + -- Documentation prefix (RFC 3849) + {"2001:db8::/32", 100, "Documentation prefix"}, + {"2001:db8:1::/48", 101, "Documentation /48 allocation"}, + {"2001:db8:1:2::/64", 102, "Documentation /64 subnet"}, + + -- Link-local (fe80::/10) + {"fe80::/10", 200, "Link-local addresses"}, + + -- Loopback (::1/128) + {"::1/128", 300, "Loopback address"}, + + -- IPv4-mapped addresses (::ffff:0:0/96) + {"::ffff:0:0/96", 400, "IPv4-mapped addresses"}, + + -- Unique Local Addresses (fc00::/7) + {"fc00::/7", 500, "Unique Local Addresses"}, + + -- Default route + {"::/0", 999, "Default route"}, +} + +print("\nInserting IPv6 routes:") +for _, route in ipairs(routes) do + local prefix, next_hop, name = route[1], route[2], route[3] + local ok, err = ipv6_table:insert(prefix, next_hop) + if ok then + print(string.format(" %-24s -> %3d (%s)", prefix, next_hop, name)) + else + print(string.format(" %-24s FAILED: %s", prefix, err)) + end +end + +-- Lookup test addresses +print("\nLooking up IPv6 addresses:") +local test_addrs = { + "2001:db8::1", -- Matches /32 -> 100 + "2001:db8:1::1", -- Matches /48 -> 101 + "2001:db8:1:2::1", -- Matches /64 -> 102 + "fe80::1", -- Matches link-local -> 200 + "::1", -- Matches loopback -> 300 + "::ffff:192.168.1.1", -- Matches IPv4-mapped -> 400 + "fd00::1", -- Matches ULA -> 500 + "2607:f8b0:4004::1", -- Matches default -> 999 (Google) +} + +for _, addr in ipairs(test_addrs) do + local next_hop = ipv6_table:lookup(addr) + if next_hop then + print(string.format(" %-24s -> %d", addr, next_hop)) + else + print(string.format(" %-24s -> no match", addr)) + end +end + +ipv6_table:close() + +-- ============================================================================ +-- Example 2: IPv6 Address Formats +-- ============================================================================ + +print("\n" .. "=" .. string.rep("=", 60)) +print("Example 2: IPv6 Address Formats") +print("-" .. string.rep("-", 40)) + +local table2 = lpm.new_ipv6() + +-- Full expanded notation +print("\nFull expanded notation:") +table2:insert("2001:0db8:0000:0000:0000:0000:0000:0000/32", 1) +print(" Inserted: 2001:0db8:0000:0000:0000:0000:0000:0000/32") + +-- Compressed notation with :: +print("\nCompressed notation:") +table2:insert("2001:db8:1::/48", 2) +print(" Inserted: 2001:db8:1::/48") + +-- Leading zeros omitted +table2:insert("2001:db8:a:b::/64", 3) +print(" Inserted: 2001:db8:a:b::/64") + +-- Verify lookups +print("\nVerification:") +print(string.format(" 2001:db8::1 -> %d", table2:lookup("2001:db8::1"))) +print(string.format(" 2001:db8:1::1 -> %d", table2:lookup("2001:db8:1::1"))) +print(string.format(" 2001:db8:a:b::1 -> %d", table2:lookup("2001:db8:a:b::1"))) + +table2:close() + +-- ============================================================================ +-- Example 3: IPv6 Longest Prefix Matching +-- ============================================================================ + +print("\n" .. "=" .. string.rep("=", 60)) +print("Example 3: IPv6 Longest Prefix Matching") +print("-" .. string.rep("-", 40)) + +local table3 = lpm.new_ipv6() + +-- Create overlapping prefixes +print("\nInserting overlapping prefixes:") +local prefixes = { + {"2001:db8::/32", 32}, + {"2001:db8:abcd::/48", 48}, + {"2001:db8:abcd:ef01::/64", 64}, + {"2001:db8:abcd:ef01:2345::/80", 80}, +} + +for _, p in ipairs(prefixes) do + table3:insert(p[1], p[2]) + print(string.format(" %s -> %d", p[1], p[2])) +end + +-- Test longest prefix matching +print("\nLongest prefix match tests:") +local tests = { + {"2001:db8::1", 32, "/32"}, + {"2001:db8:abcd::1", 48, "/48"}, + {"2001:db8:abcd:ef01::1", 64, "/64"}, + {"2001:db8:abcd:ef01:2345::1", 80, "/80"}, +} + +for _, test in ipairs(tests) do + local addr, expected, desc = test[1], test[2], test[3] + local result = table3:lookup(addr) + local status = (result == expected) and "OK" or "MISMATCH" + local result_str = result and tostring(result) or "nil" + print(string.format(" %-32s -> %s (expected %s) [%s]", + addr, result_str, desc, status)) +end + +table3:close() + +-- ============================================================================ +-- Example 4: Algorithm Comparison +-- ============================================================================ + +print("\n" .. "=" .. string.rep("=", 60)) +print("Example 4: IPv6 Algorithm Comparison") +print("-" .. string.rep("-", 40)) + +-- Wide 16-bit stride (default, good for common /48 allocations) +print("\nWide 16-bit stride algorithm (default):") +local wide16 = lpm.new_ipv6("wide16") +wide16:insert("2001:db8::/32", 100) +wide16:insert("fe80::/10", 200) +print(string.format(" 2001:db8::1 -> %d", wide16:lookup("2001:db8::1"))) +print(string.format(" fe80::1 -> %d", wide16:lookup("fe80::1"))) +wide16:close() + +-- 8-bit stride algorithm +print("\n8-bit stride algorithm:") +local stride8 = lpm.new_ipv6("stride8") +stride8:insert("2001:db8::/32", 100) +stride8:insert("fe80::/10", 200) +print(string.format(" 2001:db8::1 -> %d", stride8:lookup("2001:db8::1"))) +print(string.format(" fe80::1 -> %d", stride8:lookup("fe80::1"))) +stride8:close() + +print("\nBoth algorithms produce the same results.") +print("wide16 is optimized for common /48 allocations (ISP assignments).") +print("stride8 is simpler and may use less memory for sparse tables.") + +-- ============================================================================ +-- Example 5: IPv6 Host Routes (/128) +-- ============================================================================ + +print("\n" .. "=" .. string.rep("=", 60)) +print("Example 5: IPv6 Host Routes (/128)") +print("-" .. string.rep("-", 40)) + +local table5 = lpm.new_ipv6() + +-- Insert host-specific routes +print("\nInserting host routes:") +table5:insert("2001:db8::/32", 1) -- Network +table5:insert("2001:db8::1/128", 100) -- Specific host +table5:insert("2001:db8::2/128", 200) -- Another host + +print(" 2001:db8::/32 -> 1 (network)") +print(" 2001:db8::1/128 -> 100 (host 1)") +print(" 2001:db8::2/128 -> 200 (host 2)") + +print("\nLookups:") +print(string.format(" 2001:db8::1 -> %d (exact match)", table5:lookup("2001:db8::1"))) +print(string.format(" 2001:db8::2 -> %d (exact match)", table5:lookup("2001:db8::2"))) +print(string.format(" 2001:db8::3 -> %d (falls back to /32)", table5:lookup("2001:db8::3"))) + +table5:close() + +-- ============================================================================ +-- Summary +-- ============================================================================ + +print("\n" .. "=" .. string.rep("=", 60)) +print("IPv6 Example complete!") +print("=" .. string.rep("=", 60)) diff --git a/bindings/lua/liblpm-2.0.0-1.rockspec b/bindings/lua/liblpm-2.0.0-1.rockspec new file mode 100644 index 0000000..3620b04 --- /dev/null +++ b/bindings/lua/liblpm-2.0.0-1.rockspec @@ -0,0 +1,79 @@ +-- LuaRocks rockspec for liblpm Lua bindings +-- https://github.com/MuriloChianfa/liblpm +-- +-- Installation: +-- luarocks make liblpm-2.0.0-1.rockspec +-- +-- Requirements: +-- - liblpm C library must be installed (pkg-config detectable) +-- - Lua 5.3+ or LuaJIT 2.1+ + +rockspec_format = "3.0" +package = "liblpm" +version = "2.0.0-1" + +source = { + url = "git+https://github.com/MuriloChianfa/liblpm.git", + tag = "v2.0.0" +} + +description = { + summary = "High-performance Longest Prefix Match library for Lua", + detailed = [[ + Lua bindings for liblpm, a high-performance C library for Longest Prefix + Match (LPM) lookups. Supports both IPv4 and IPv6 with multiple optimized + algorithms (DIR-24-8, 8-bit stride, 16-bit wide stride). + + Features: + - IPv4 and IPv6 support with algorithm selection + - Multiple input formats: CIDR strings, dotted-decimal, byte tables + - Batch lookup operations for high throughput + - Automatic memory management with explicit close() option + - Both object-oriented and functional API styles + ]], + homepage = "https://github.com/MuriloChianfa/liblpm", + license = "BSL-1.0", + labels = {"networking", "routing", "ip", "lpm", "trie"}, + maintainer = "Murilo Chianfa " +} + +dependencies = { + "lua >= 5.3" +} + +external_dependencies = { + LIBLPM = { + header = "lpm/lpm.h", + library = "lpm" + } +} + +build = { + type = "builtin", + modules = { + liblpm = { + sources = { + "src/liblpm.c", + "src/liblpm_utils.c" + }, + libraries = {"lpm"}, + incdirs = {"$(LIBLPM_INCDIR)"}, + libdirs = {"$(LIBLPM_LIBDIR)"} + } + }, + copy_directories = { + "examples", + "tests" + } +} + +test_dependencies = { + "lua >= 5.3" +} + +-- Note: Due to ifunc resolvers in liblpm, you may need to run tests with: +-- LD_PRELOAD=/usr/local/lib/liblpm.so luarocks test +test = { + type = "command", + command = "LD_PRELOAD=/usr/local/lib/liblpm.so lua tests/test_lpm.lua" +} diff --git a/bindings/lua/src/liblpm.c b/bindings/lua/src/liblpm.c new file mode 100644 index 0000000..8799262 --- /dev/null +++ b/bindings/lua/src/liblpm.c @@ -0,0 +1,794 @@ +/** + * @file liblpm.c + * @brief Lua bindings for liblpm - High-Performance Longest Prefix Match library + * + * This module provides Lua bindings for the liblpm C library, supporting both + * IPv4 and IPv6 longest prefix match operations with multiple algorithm options. + * + * Supports: Lua 5.3, 5.4, and LuaJIT 2.1+ + * + * @author Murilo Chianfa + * @license Boost Software License 1.0 + */ + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +/* Compatibility macros for different Lua versions */ +#if LUA_VERSION_NUM < 502 + #define luaL_newlib(L, l) (lua_newtable(L), luaL_register(L, NULL, l)) + #define luaL_setfuncs(L, l, n) luaL_register(L, NULL, l) +#endif + +#if LUA_VERSION_NUM < 503 + /* lua_isinteger doesn't exist in Lua 5.1/5.2 */ + #define lua_isinteger(L, idx) (lua_type(L, idx) == LUA_TNUMBER) +#endif + +/* Metatable names */ +#define LPM_TABLE_MT "liblpm.table" + +/* Module version */ +#define LPM_LUA_VERSION "2.0.0" + +/* Maximum batch size to prevent excessive memory allocation */ +#define LPM_MAX_BATCH_SIZE 100000 + +/* ============================================================================ + * Userdata Structure + * ============================================================================ */ + +typedef struct { + lpm_trie_t *trie; + bool is_ipv6; + bool closed; +} lua_lpm_table_t; + +/* ============================================================================ + * Forward declarations for utility functions + * ============================================================================ */ + +/* IP address parsing (implemented in liblpm_utils.c) */ +int lpm_lua_parse_ipv4(const char *str, uint8_t out[4]); +int lpm_lua_parse_ipv6(const char *str, uint8_t out[16]); +int lpm_lua_parse_ipv4_cidr(const char *str, uint8_t prefix[4], uint8_t *prefix_len); +int lpm_lua_parse_ipv6_cidr(const char *str, uint8_t prefix[16], uint8_t *prefix_len); + +/* ============================================================================ + * Helper Functions + * ============================================================================ */ + +/** + * Get the lua_lpm_table_t userdata from the stack at the given index. + * Raises a Lua error if the argument is not a valid lpm table. + */ +static lua_lpm_table_t *check_lpm_table(lua_State *L, int idx) { + lua_lpm_table_t *t = (lua_lpm_table_t *)luaL_checkudata(L, idx, LPM_TABLE_MT); + if (t->closed) { + luaL_error(L, "attempt to use a closed lpm table"); + } + return t; +} + +/** + * Get the lua_lpm_table_t userdata without checking if closed. + * Used for operations that are valid on closed tables (like is_closed). + */ +static lua_lpm_table_t *get_lpm_table(lua_State *L, int idx) { + return (lua_lpm_table_t *)luaL_checkudata(L, idx, LPM_TABLE_MT); +} + +/** + * Parse an IPv4 address from various Lua formats: + * - String: "192.168.1.1" + * - Table: {192, 168, 1, 1} + * - Binary string: 4-byte string + */ +static int parse_ipv4_address(lua_State *L, int idx, uint8_t out[4]) { + if (lua_isstring(L, idx)) { + size_t len; + const char *str = lua_tolstring(L, idx, &len); + + /* Binary string (4 bytes) */ + if (len == 4) { + memcpy(out, str, 4); + return 0; + } + + /* Dotted-decimal string */ + if (lpm_lua_parse_ipv4(str, out) != 0) { + return -1; + } + return 0; + } + else if (lua_istable(L, idx)) { + for (int i = 0; i < 4; i++) { + lua_rawgeti(L, idx, i + 1); + if (!lua_isinteger(L, -1)) { + lua_pop(L, 1); + return -1; + } + lua_Integer val = lua_tointeger(L, -1); + lua_pop(L, 1); + if (val < 0 || val > 255) { + return -1; + } + out[i] = (uint8_t)val; + } + return 0; + } + + return -1; +} + +/** + * Parse an IPv6 address from various Lua formats: + * - String: "2001:db8::1" + * - Table: {0x20, 0x01, 0x0d, 0xb8, ...} (16 bytes) + * - Binary string: 16-byte string + */ +static int parse_ipv6_address(lua_State *L, int idx, uint8_t out[16]) { + if (lua_isstring(L, idx)) { + size_t len; + const char *str = lua_tolstring(L, idx, &len); + + /* Binary string (16 bytes) */ + if (len == 16) { + memcpy(out, str, 16); + return 0; + } + + /* Colon-hex string */ + if (lpm_lua_parse_ipv6(str, out) != 0) { + return -1; + } + return 0; + } + else if (lua_istable(L, idx)) { + for (int i = 0; i < 16; i++) { + lua_rawgeti(L, idx, i + 1); + if (!lua_isinteger(L, -1)) { + lua_pop(L, 1); + return -1; + } + lua_Integer val = lua_tointeger(L, -1); + lua_pop(L, 1); + if (val < 0 || val > 255) { + return -1; + } + out[i] = (uint8_t)val; + } + return 0; + } + + return -1; +} + +/** + * Parse a prefix (address + prefix length) from various formats: + * - CIDR string: "192.168.0.0/16" + * - Address + separate prefix_len argument + */ +static int parse_prefix(lua_State *L, int idx, bool is_ipv6, + uint8_t *prefix, uint8_t *prefix_len) { + int addr_size = is_ipv6 ? 16 : 4; + + /* Check if it's a CIDR string */ + if (lua_isstring(L, idx)) { + const char *str = lua_tostring(L, idx); + + /* Check for '/' indicating CIDR notation */ + if (strchr(str, '/') != NULL) { + if (is_ipv6) { + return lpm_lua_parse_ipv6_cidr(str, prefix, prefix_len); + } else { + return lpm_lua_parse_ipv4_cidr(str, prefix, prefix_len); + } + } + + /* Plain address, need prefix_len from next argument */ + if (is_ipv6) { + if (lpm_lua_parse_ipv6(str, prefix) != 0) { + return -1; + } + } else { + if (lpm_lua_parse_ipv4(str, prefix) != 0) { + return -1; + } + } + + /* Get prefix length from next argument */ + if (!lua_isinteger(L, idx + 1)) { + return -1; + } + lua_Integer len = lua_tointeger(L, idx + 1); + int max_len = is_ipv6 ? 128 : 32; + if (len < 0 || len > max_len) { + return -1; + } + *prefix_len = (uint8_t)len; + return 1; /* Consumed 2 arguments */ + } + else if (lua_istable(L, idx)) { + /* Table of bytes */ + if (is_ipv6) { + if (parse_ipv6_address(L, idx, prefix) != 0) { + return -1; + } + } else { + if (parse_ipv4_address(L, idx, prefix) != 0) { + return -1; + } + } + + /* Get prefix length from next argument */ + if (!lua_isinteger(L, idx + 1)) { + return -1; + } + lua_Integer len = lua_tointeger(L, idx + 1); + int max_len = is_ipv6 ? 128 : 32; + if (len < 0 || len > max_len) { + return -1; + } + *prefix_len = (uint8_t)len; + return 1; /* Consumed 2 arguments */ + } + + return -1; +} + +/* ============================================================================ + * Table Creation Functions + * ============================================================================ */ + +/** + * lpm.new_ipv4([algorithm]) -> table + * + * Create a new IPv4 LPM table. + * + * @param algorithm (optional) "dir24" (default) or "stride8" + * @return LPM table userdata + */ +static int l_new_ipv4(lua_State *L) { + const char *algo = luaL_optstring(L, 1, "dir24"); + + lpm_trie_t *trie = NULL; + + if (strcmp(algo, "dir24") == 0) { + trie = lpm_create_ipv4_dir24(); + } else if (strcmp(algo, "stride8") == 0) { + trie = lpm_create_ipv4_8stride(); + } else { + return luaL_error(L, "invalid algorithm '%s' (expected 'dir24' or 'stride8')", algo); + } + + if (trie == NULL) { + return luaL_error(L, "failed to create IPv4 LPM table: out of memory"); + } + + lua_lpm_table_t *t = (lua_lpm_table_t *)lua_newuserdata(L, sizeof(lua_lpm_table_t)); + t->trie = trie; + t->is_ipv6 = false; + t->closed = false; + + luaL_getmetatable(L, LPM_TABLE_MT); + lua_setmetatable(L, -2); + + return 1; +} + +/** + * lpm.new_ipv6([algorithm]) -> table + * + * Create a new IPv6 LPM table. + * + * @param algorithm (optional) "wide16" (default) or "stride8" + * @return LPM table userdata + */ +static int l_new_ipv6(lua_State *L) { + const char *algo = luaL_optstring(L, 1, "wide16"); + + lpm_trie_t *trie = NULL; + + if (strcmp(algo, "wide16") == 0) { + trie = lpm_create_ipv6_wide16(); + } else if (strcmp(algo, "stride8") == 0) { + trie = lpm_create_ipv6_8stride(); + } else { + return luaL_error(L, "invalid algorithm '%s' (expected 'wide16' or 'stride8')", algo); + } + + if (trie == NULL) { + return luaL_error(L, "failed to create IPv6 LPM table: out of memory"); + } + + lua_lpm_table_t *t = (lua_lpm_table_t *)lua_newuserdata(L, sizeof(lua_lpm_table_t)); + t->trie = trie; + t->is_ipv6 = true; + t->closed = false; + + luaL_getmetatable(L, LPM_TABLE_MT); + lua_setmetatable(L, -2); + + return 1; +} + +/* ============================================================================ + * Table Methods + * ============================================================================ */ + +/** + * table:insert(prefix, prefix_len, next_hop) -> boolean, [error] + * table:insert(cidr_string, next_hop) -> boolean, [error] + * + * Insert a route into the LPM table. + * + * @param prefix IP prefix (string, table of bytes, or CIDR string) + * @param prefix_len Prefix length (0-32 for IPv4, 0-128 for IPv6) + * @param next_hop Next hop value (unsigned 32-bit integer, max 30 bits for dir24) + * @return true on success, nil + error message on failure + */ +static int l_insert(lua_State *L) { + lua_lpm_table_t *t = check_lpm_table(L, 1); + + uint8_t prefix[16] = {0}; + uint8_t prefix_len = 0; + + int consumed = parse_prefix(L, 2, t->is_ipv6, prefix, &prefix_len); + if (consumed < 0) { + lua_pushnil(L); + lua_pushstring(L, "invalid prefix format"); + return 2; + } + + /* Get next_hop from the appropriate position */ + int nh_idx = (consumed == 0) ? 3 : 4; + if (!lua_isinteger(L, nh_idx)) { + lua_pushnil(L); + lua_pushstring(L, "next_hop must be an integer"); + return 2; + } + + lua_Integer next_hop = lua_tointeger(L, nh_idx); + if (next_hop < 0 || next_hop > 0x3FFFFFFF) { + lua_pushnil(L); + lua_pushstring(L, "next_hop must be between 0 and 0x3FFFFFFF (30-bit value)"); + return 2; + } + + int result = lpm_add(t->trie, prefix, prefix_len, (uint32_t)next_hop); + if (result != 0) { + lua_pushnil(L); + lua_pushstring(L, "insert failed"); + return 2; + } + + lua_pushboolean(L, 1); + return 1; +} + +/** + * table:delete(prefix, prefix_len) -> boolean, [error] + * table:delete(cidr_string) -> boolean, [error] + * + * Delete a route from the LPM table. + * + * @param prefix IP prefix (string, table of bytes, or CIDR string) + * @param prefix_len Prefix length (required unless using CIDR notation) + * @return true on success, nil + error message on failure + */ +static int l_delete(lua_State *L) { + lua_lpm_table_t *t = check_lpm_table(L, 1); + + uint8_t prefix[16] = {0}; + uint8_t prefix_len = 0; + + int consumed = parse_prefix(L, 2, t->is_ipv6, prefix, &prefix_len); + if (consumed < 0) { + lua_pushnil(L); + lua_pushstring(L, "invalid prefix format"); + return 2; + } + + int result = lpm_delete(t->trie, prefix, prefix_len); + if (result != 0) { + lua_pushnil(L); + lua_pushstring(L, "delete failed (prefix not found)"); + return 2; + } + + lua_pushboolean(L, 1); + return 1; +} + +/** + * table:lookup(address) -> next_hop or nil + * + * Look up an IP address in the LPM table. + * + * @param address IP address (string, table of bytes, or binary string) + * @return next_hop value if found, nil if not found + */ +static int l_lookup(lua_State *L) { + lua_lpm_table_t *t = check_lpm_table(L, 1); + + uint32_t result; + + if (t->is_ipv6) { + uint8_t addr[16]; + if (parse_ipv6_address(L, 2, addr) != 0) { + return luaL_error(L, "invalid IPv6 address format"); + } + result = lpm_lookup_ipv6(t->trie, addr); + } else { + uint8_t addr[4]; + if (parse_ipv4_address(L, 2, addr) != 0) { + return luaL_error(L, "invalid IPv4 address format"); + } + /* Convert to host byte order uint32 */ + uint32_t addr32 = ((uint32_t)addr[0] << 24) | ((uint32_t)addr[1] << 16) | + ((uint32_t)addr[2] << 8) | (uint32_t)addr[3]; + result = lpm_lookup_ipv4(t->trie, addr32); + } + + if (result == LPM_INVALID_NEXT_HOP) { + lua_pushnil(L); + return 1; + } + + lua_pushinteger(L, result); + return 1; +} + +/** + * table:lookup_batch(addresses) -> table of results + * + * Look up multiple IP addresses in the LPM table. + * + * @param addresses Table of IP addresses + * @return Table where results[i] = next_hop or nil for addresses[i] + */ +static int l_lookup_batch(lua_State *L) { + lua_lpm_table_t *t = check_lpm_table(L, 1); + + luaL_checktype(L, 2, LUA_TTABLE); + + /* Get table length */ + lua_len(L, 2); + lua_Integer count = lua_tointeger(L, -1); + lua_pop(L, 1); + + if (count <= 0) { + lua_newtable(L); + return 1; + } + + if (count > LPM_MAX_BATCH_SIZE) { + return luaL_error(L, "batch size too large (max %d)", LPM_MAX_BATCH_SIZE); + } + + /* Allocate arrays for addresses and results */ + uint32_t *next_hops = (uint32_t *)malloc(count * sizeof(uint32_t)); + if (next_hops == NULL) { + return luaL_error(L, "out of memory"); + } + + /* Create result table */ + lua_createtable(L, (int)count, 0); + + if (t->is_ipv6) { + uint8_t (*addrs)[16] = (uint8_t (*)[16])malloc(count * 16); + if (addrs == NULL) { + free(next_hops); + return luaL_error(L, "out of memory"); + } + + /* Parse all addresses */ + for (lua_Integer i = 0; i < count; i++) { + lua_rawgeti(L, 2, i + 1); + if (parse_ipv6_address(L, -1, addrs[i]) != 0) { + free(addrs); + free(next_hops); + return luaL_error(L, "invalid IPv6 address at index %d", (int)(i + 1)); + } + lua_pop(L, 1); + } + + /* Batch lookup */ + lpm_lookup_batch_ipv6(t->trie, (const uint8_t (*)[16])addrs, next_hops, count); + + free(addrs); + } else { + uint32_t *addrs = (uint32_t *)malloc(count * sizeof(uint32_t)); + if (addrs == NULL) { + free(next_hops); + return luaL_error(L, "out of memory"); + } + + /* Parse all addresses */ + for (lua_Integer i = 0; i < count; i++) { + lua_rawgeti(L, 2, i + 1); + uint8_t addr_bytes[4]; + if (parse_ipv4_address(L, -1, addr_bytes) != 0) { + free(addrs); + free(next_hops); + return luaL_error(L, "invalid IPv4 address at index %d", (int)(i + 1)); + } + lua_pop(L, 1); + + /* Convert to host byte order uint32 */ + addrs[i] = ((uint32_t)addr_bytes[0] << 24) | ((uint32_t)addr_bytes[1] << 16) | + ((uint32_t)addr_bytes[2] << 8) | (uint32_t)addr_bytes[3]; + } + + /* Batch lookup */ + lpm_lookup_batch_ipv4(t->trie, addrs, next_hops, count); + + free(addrs); + } + + /* Build result table */ + for (lua_Integer i = 0; i < count; i++) { + if (next_hops[i] == LPM_INVALID_NEXT_HOP) { + /* Leave as nil (don't set anything) */ + } else { + lua_pushinteger(L, next_hops[i]); + lua_rawseti(L, -2, i + 1); + } + } + + free(next_hops); + return 1; +} + +/** + * table:close() -> void + * + * Explicitly close and release resources of the LPM table. + * Safe to call multiple times. + */ +static int l_close(lua_State *L) { + lua_lpm_table_t *t = get_lpm_table(L, 1); + + if (!t->closed && t->trie != NULL) { + lpm_destroy(t->trie); + t->trie = NULL; + t->closed = true; + } + + return 0; +} + +/** + * table:is_closed() -> boolean + * + * Check if the table has been closed. + * + * @return true if closed, false otherwise + */ +static int l_is_closed(lua_State *L) { + lua_lpm_table_t *t = get_lpm_table(L, 1); + lua_pushboolean(L, t->closed); + return 1; +} + +/** + * table:is_ipv6() -> boolean + * + * Check if the table is for IPv6 addresses. + * + * @return true if IPv6, false if IPv4 + */ +static int l_is_ipv6(lua_State *L) { + lua_lpm_table_t *t = get_lpm_table(L, 1); + lua_pushboolean(L, t->is_ipv6); + return 1; +} + +/** + * __gc metamethod - garbage collection finalizer + */ +static int l_gc(lua_State *L) { + lua_lpm_table_t *t = get_lpm_table(L, 1); + + if (!t->closed && t->trie != NULL) { + lpm_destroy(t->trie); + t->trie = NULL; + t->closed = true; + } + + return 0; +} + +/** + * __tostring metamethod + */ +static int l_tostring(lua_State *L) { + lua_lpm_table_t *t = get_lpm_table(L, 1); + + if (t->closed) { + lua_pushstring(L, "lpm.table (closed)"); + } else { + lua_pushfstring(L, "lpm.table (%s)", t->is_ipv6 ? "IPv6" : "IPv4"); + } + + return 1; +} + +/* ============================================================================ + * Module Functions (functional style wrappers) + * ============================================================================ */ + +/** + * lpm.insert(table, ...) -> boolean, [error] + * + * Functional-style wrapper for table:insert() + */ +static int l_mod_insert(lua_State *L) { + return l_insert(L); +} + +/** + * lpm.delete(table, ...) -> boolean, [error] + * + * Functional-style wrapper for table:delete() + */ +static int l_mod_delete(lua_State *L) { + return l_delete(L); +} + +/** + * lpm.lookup(table, address) -> next_hop or nil + * + * Functional-style wrapper for table:lookup() + */ +static int l_mod_lookup(lua_State *L) { + return l_lookup(L); +} + +/** + * lpm.lookup_batch(table, addresses) -> results + * + * Functional-style wrapper for table:lookup_batch() + */ +static int l_mod_lookup_batch(lua_State *L) { + return l_lookup_batch(L); +} + +/** + * lpm.close(table) + * + * Functional-style wrapper for table:close() + */ +static int l_mod_close(lua_State *L) { + return l_close(L); +} + +/** + * lpm.is_closed(table) -> boolean + * + * Functional-style wrapper for table:is_closed() + */ +static int l_mod_is_closed(lua_State *L) { + return l_is_closed(L); +} + +/* ============================================================================ + * Utility Functions + * ============================================================================ */ + +/** + * lpm.version() -> string + * + * Get the library version string. + * + * @return Version string + */ +static int l_version(lua_State *L) { + lua_pushstring(L, lpm_get_version()); + return 1; +} + +/** + * lpm.lua_version() -> string + * + * Get the Lua binding version string. + * + * @return Lua binding version string + */ +static int l_lua_version(lua_State *L) { + lua_pushstring(L, LPM_LUA_VERSION); + return 1; +} + +/* ============================================================================ + * Module Registration + * ============================================================================ */ + +/* Metatable methods for lpm.table */ +static const luaL_Reg lpm_table_methods[] = { + {"insert", l_insert}, + {"delete", l_delete}, + {"lookup", l_lookup}, + {"lookup_batch", l_lookup_batch}, + {"close", l_close}, + {"is_closed", l_is_closed}, + {"is_ipv6", l_is_ipv6}, + {NULL, NULL} +}; + +/* Module functions */ +static const luaL_Reg lpm_module_funcs[] = { + /* Table creation */ + {"new_ipv4", l_new_ipv4}, + {"new_ipv6", l_new_ipv6}, + + /* Functional-style wrappers */ + {"insert", l_mod_insert}, + {"delete", l_mod_delete}, + {"lookup", l_mod_lookup}, + {"lookup_batch", l_mod_lookup_batch}, + {"close", l_mod_close}, + {"is_closed", l_mod_is_closed}, + + /* Utility functions */ + {"version", l_version}, + {"lua_version", l_lua_version}, + + {NULL, NULL} +}; + +/** + * Module initialization function. + * + * Called when the module is loaded via require("liblpm"). + */ +int luaopen_liblpm(lua_State *L) { + /* Create metatable for lpm.table userdata */ + luaL_newmetatable(L, LPM_TABLE_MT); + + /* Set __index to self for method access */ + lua_pushvalue(L, -1); + lua_setfield(L, -2, "__index"); + + /* Set __gc for garbage collection */ + lua_pushcfunction(L, l_gc); + lua_setfield(L, -2, "__gc"); + + /* Set __tostring */ + lua_pushcfunction(L, l_tostring); + lua_setfield(L, -2, "__tostring"); + + /* Register methods */ + luaL_setfuncs(L, lpm_table_methods, 0); + + lua_pop(L, 1); /* Pop metatable */ + + /* Create module table */ + luaL_newlib(L, lpm_module_funcs); + + /* Add constants */ + lua_pushinteger(L, LPM_INVALID_NEXT_HOP); + lua_setfield(L, -2, "INVALID_NEXT_HOP"); + + lua_pushinteger(L, LPM_IPV4_MAX_DEPTH); + lua_setfield(L, -2, "IPV4_MAX_DEPTH"); + + lua_pushinteger(L, LPM_IPV6_MAX_DEPTH); + lua_setfield(L, -2, "IPV6_MAX_DEPTH"); + + /* Add version string */ + lua_pushstring(L, LPM_LUA_VERSION); + lua_setfield(L, -2, "_VERSION"); + + return 1; +} diff --git a/bindings/lua/src/liblpm_utils.c b/bindings/lua/src/liblpm_utils.c new file mode 100644 index 0000000..1315a3d --- /dev/null +++ b/bindings/lua/src/liblpm_utils.c @@ -0,0 +1,353 @@ +/** + * @file liblpm_utils.c + * @brief Utility functions for Lua bindings - IP address and CIDR parsing + * + * This file provides functions to parse IPv4 and IPv6 addresses in various + * formats (dotted-decimal, colon-hex, CIDR notation). + * + * @author Murilo Chianfa + * @license Boost Software License 1.0 + */ + +#include +#include +#include +#include +#include +#include +#include + +/* ============================================================================ + * IPv4 Parsing + * ============================================================================ */ + +/** + * Parse an IPv4 address in dotted-decimal notation. + * + * @param str Input string (e.g., "192.168.1.1") + * @param out Output buffer (4 bytes) + * @return 0 on success, -1 on error + */ +int lpm_lua_parse_ipv4(const char *str, uint8_t out[4]) { + if (str == NULL || out == NULL) { + return -1; + } + + unsigned int a, b, c, d; + char extra; + + /* Parse dotted-decimal format */ + int n = sscanf(str, "%u.%u.%u.%u%c", &a, &b, &c, &d, &extra); + + if (n != 4) { + return -1; + } + + /* Validate ranges */ + if (a > 255 || b > 255 || c > 255 || d > 255) { + return -1; + } + + out[0] = (uint8_t)a; + out[1] = (uint8_t)b; + out[2] = (uint8_t)c; + out[3] = (uint8_t)d; + + return 0; +} + +/** + * Parse an IPv4 CIDR notation string. + * + * @param str Input string (e.g., "192.168.0.0/16") + * @param prefix Output prefix buffer (4 bytes) + * @param prefix_len Output prefix length + * @return 0 on success, -1 on error + */ +int lpm_lua_parse_ipv4_cidr(const char *str, uint8_t prefix[4], uint8_t *prefix_len) { + if (str == NULL || prefix == NULL || prefix_len == NULL) { + return -1; + } + + /* Find the '/' separator */ + const char *slash = strchr(str, '/'); + if (slash == NULL) { + return -1; + } + + /* Parse the address part */ + size_t addr_len = slash - str; + if (addr_len >= 16) { /* Max IPv4 string length */ + return -1; + } + + char addr_buf[16]; + memcpy(addr_buf, str, addr_len); + addr_buf[addr_len] = '\0'; + + if (lpm_lua_parse_ipv4(addr_buf, prefix) != 0) { + return -1; + } + + /* Parse the prefix length */ + const char *len_str = slash + 1; + if (*len_str == '\0') { + return -1; + } + + char *endptr; + errno = 0; + long len = strtol(len_str, &endptr, 10); + + if (errno != 0 || *endptr != '\0' || len < 0 || len > 32) { + return -1; + } + + *prefix_len = (uint8_t)len; + + /* Zero out bits beyond the prefix length */ + if (*prefix_len < 32) { + int full_bytes = *prefix_len / 8; + int remaining_bits = *prefix_len % 8; + + if (remaining_bits > 0) { + uint8_t mask = (uint8_t)(0xFF << (8 - remaining_bits)); + prefix[full_bytes] &= mask; + full_bytes++; + } + + for (int i = full_bytes; i < 4; i++) { + prefix[i] = 0; + } + } + + return 0; +} + +/* ============================================================================ + * IPv6 Parsing + * ============================================================================ */ + +/** + * Parse a 16-bit hexadecimal group. + * + * @param str Input string + * @param end Pointer to end of parsed portion + * @param value Output value + * @return 0 on success, -1 on error + */ +static int parse_hex_group(const char *str, const char **end, uint16_t *value) { + if (str == NULL || !isxdigit((unsigned char)*str)) { + return -1; + } + + char *endptr; + errno = 0; + unsigned long val = strtoul(str, &endptr, 16); + + if (errno != 0 || endptr == str || val > 0xFFFF) { + return -1; + } + + /* Check that we didn't consume more than 4 hex digits */ + if (endptr - str > 4) { + return -1; + } + + *value = (uint16_t)val; + *end = endptr; + return 0; +} + +/** + * Parse an IPv6 address in colon-hex notation. + * + * Supports: + * - Full notation: "2001:0db8:0000:0000:0000:0000:0000:0001" + * - Compressed: "2001:db8::1" + * - Mixed: "::ffff:192.168.1.1" + * + * @param str Input string + * @param out Output buffer (16 bytes) + * @return 0 on success, -1 on error + */ +int lpm_lua_parse_ipv6(const char *str, uint8_t out[16]) { + if (str == NULL || out == NULL) { + return -1; + } + + memset(out, 0, 16); + + uint16_t groups[8] = {0}; + int num_groups = 0; + int double_colon_pos = -1; + const char *p = str; + + /* Handle leading :: */ + if (p[0] == ':' && p[1] == ':') { + double_colon_pos = 0; + p += 2; + if (*p == '\0') { + /* Just "::" - all zeros */ + return 0; + } + } else if (*p == ':') { + /* Single leading colon is invalid */ + return -1; + } + + while (*p != '\0' && num_groups < 8) { + /* Check for :: */ + if (p[0] == ':' && p[1] == ':') { + if (double_colon_pos >= 0) { + /* Only one :: allowed */ + return -1; + } + double_colon_pos = num_groups; + p += 2; + if (*p == '\0') { + break; + } + continue; + } + + /* Skip single colon between groups */ + if (*p == ':') { + p++; + } + + /* Check for embedded IPv4 (only valid in last 32 bits) */ + const char *dot = strchr(p, '.'); + if (dot != NULL && (dot - p) <= 3) { + /* Parse embedded IPv4 */ + uint8_t ipv4[4]; + if (lpm_lua_parse_ipv4(p, ipv4) != 0) { + return -1; + } + + /* IPv4 takes 2 groups */ + if (num_groups > 6) { + return -1; + } + + groups[num_groups++] = ((uint16_t)ipv4[0] << 8) | ipv4[1]; + groups[num_groups++] = ((uint16_t)ipv4[2] << 8) | ipv4[3]; + break; + } + + /* Parse hex group */ + const char *end; + uint16_t value; + if (parse_hex_group(p, &end, &value) != 0) { + return -1; + } + + groups[num_groups++] = value; + p = end; + + if (*p == '\0' || (*p == '/' || *p == '%')) { + break; + } + + if (*p != ':') { + return -1; + } + } + + /* Expand :: if present */ + if (double_colon_pos >= 0) { + int num_zeros = 8 - num_groups; + if (num_zeros < 0) { + return -1; + } + + /* Shift groups after :: */ + int groups_after = num_groups - double_colon_pos; + for (int i = 7; i >= double_colon_pos + num_zeros; i--) { + groups[i] = groups[i - num_zeros]; + } + + /* Fill with zeros */ + for (int i = double_colon_pos; i < double_colon_pos + num_zeros; i++) { + groups[i] = 0; + } + } else if (num_groups != 8) { + return -1; + } + + /* Convert to bytes */ + for (int i = 0; i < 8; i++) { + out[i * 2] = (uint8_t)(groups[i] >> 8); + out[i * 2 + 1] = (uint8_t)(groups[i] & 0xFF); + } + + return 0; +} + +/** + * Parse an IPv6 CIDR notation string. + * + * @param str Input string (e.g., "2001:db8::/32") + * @param prefix Output prefix buffer (16 bytes) + * @param prefix_len Output prefix length + * @return 0 on success, -1 on error + */ +int lpm_lua_parse_ipv6_cidr(const char *str, uint8_t prefix[16], uint8_t *prefix_len) { + if (str == NULL || prefix == NULL || prefix_len == NULL) { + return -1; + } + + /* Find the '/' separator */ + const char *slash = strchr(str, '/'); + if (slash == NULL) { + return -1; + } + + /* Parse the address part */ + size_t addr_len = slash - str; + if (addr_len >= 46) { /* Max IPv6 string length */ + return -1; + } + + char addr_buf[46]; + memcpy(addr_buf, str, addr_len); + addr_buf[addr_len] = '\0'; + + if (lpm_lua_parse_ipv6(addr_buf, prefix) != 0) { + return -1; + } + + /* Parse the prefix length */ + const char *len_str = slash + 1; + if (*len_str == '\0') { + return -1; + } + + char *endptr; + errno = 0; + long len = strtol(len_str, &endptr, 10); + + if (errno != 0 || *endptr != '\0' || len < 0 || len > 128) { + return -1; + } + + *prefix_len = (uint8_t)len; + + /* Zero out bits beyond the prefix length */ + if (*prefix_len < 128) { + int full_bytes = *prefix_len / 8; + int remaining_bits = *prefix_len % 8; + + if (remaining_bits > 0) { + uint8_t mask = (uint8_t)(0xFF << (8 - remaining_bits)); + prefix[full_bytes] &= mask; + full_bytes++; + } + + for (int i = full_bytes; i < 16; i++) { + prefix[i] = 0; + } + } + + return 0; +} diff --git a/bindings/lua/tests/test_lpm.lua b/bindings/lua/tests/test_lpm.lua new file mode 100644 index 0000000..31b559e --- /dev/null +++ b/bindings/lua/tests/test_lpm.lua @@ -0,0 +1,680 @@ +#!/usr/bin/env lua +-- ============================================================================ +-- test_lpm.lua - Comprehensive test suite for liblpm Lua bindings +-- ============================================================================ +-- +-- Run: lua tests/test_lpm.lua +-- Or via CMake: make lua_test +-- +-- ============================================================================ + +local lpm = require("liblpm") + +-- ============================================================================ +-- Test Framework +-- ============================================================================ + +local tests = {} +local passed = 0 +local failed = 0 +local current_test = nil + +local function test(name, fn) + tests[#tests + 1] = {name = name, fn = fn} +end + +local function assert_eq(actual, expected, msg) + if actual ~= expected then + error(string.format("%s: expected %s, got %s", + msg or "assertion failed", + tostring(expected), + tostring(actual)), 2) + end +end + +local function assert_nil(value, msg) + if value ~= nil then + error(string.format("%s: expected nil, got %s", + msg or "assertion failed", + tostring(value)), 2) + end +end + +local function assert_not_nil(value, msg) + if value == nil then + error((msg or "assertion failed") .. ": expected non-nil value", 2) + end +end + +local function assert_true(value, msg) + if not value then + error((msg or "assertion failed") .. ": expected true", 2) + end +end + +local function assert_false(value, msg) + if value then + error((msg or "assertion failed") .. ": expected false", 2) + end +end + +local function assert_error(fn, msg) + local ok, err = pcall(fn) + if ok then + error((msg or "assertion failed") .. ": expected error", 2) + end + return err +end + +local function run_tests() + print(string.format("Running %d tests...\n", #tests)) + + for _, t in ipairs(tests) do + current_test = t.name + io.write(string.format(" %-50s ", t.name)) + io.flush() + + local ok, err = pcall(t.fn) + if ok then + passed = passed + 1 + print("[PASS]") + else + failed = failed + 1 + print("[FAIL]") + print(string.format(" Error: %s", tostring(err))) + end + end + + print(string.format("\n%d passed, %d failed, %d total", + passed, failed, passed + failed)) + + if failed > 0 then + os.exit(1) + end +end + +-- ============================================================================ +-- Module Tests +-- ============================================================================ + +test("module loads correctly", function() + assert_not_nil(lpm, "lpm module should load") + assert_not_nil(lpm.new_ipv4, "new_ipv4 function should exist") + assert_not_nil(lpm.new_ipv6, "new_ipv6 function should exist") + assert_not_nil(lpm.version, "version function should exist") +end) + +test("version returns string", function() + local ver = lpm.version() + assert_not_nil(ver, "version should not be nil") + assert_eq(type(ver), "string", "version should be a string") + assert_true(#ver > 0, "version should not be empty") +end) + +test("constants are defined", function() + assert_not_nil(lpm.INVALID_NEXT_HOP, "INVALID_NEXT_HOP should be defined") + assert_eq(lpm.INVALID_NEXT_HOP, 0xFFFFFFFF, "INVALID_NEXT_HOP value") + assert_eq(lpm.IPV4_MAX_DEPTH, 32, "IPV4_MAX_DEPTH value") + assert_eq(lpm.IPV6_MAX_DEPTH, 128, "IPV6_MAX_DEPTH value") +end) + +-- ============================================================================ +-- IPv4 Table Creation Tests +-- ============================================================================ + +test("create IPv4 table (default algorithm)", function() + local t = lpm.new_ipv4() + assert_not_nil(t, "table should be created") + assert_false(t:is_closed(), "table should not be closed") + assert_false(t:is_ipv6(), "table should be IPv4") + t:close() +end) + +test("create IPv4 table (dir24 algorithm)", function() + local t = lpm.new_ipv4("dir24") + assert_not_nil(t, "table should be created") + t:close() +end) + +test("create IPv4 table (stride8 algorithm)", function() + local t = lpm.new_ipv4("stride8") + assert_not_nil(t, "table should be created") + t:close() +end) + +test("create IPv4 table with invalid algorithm fails", function() + assert_error(function() + lpm.new_ipv4("invalid") + end, "invalid algorithm should error") +end) + +-- ============================================================================ +-- IPv6 Table Creation Tests +-- ============================================================================ + +test("create IPv6 table (default algorithm)", function() + local t = lpm.new_ipv6() + assert_not_nil(t, "table should be created") + assert_false(t:is_closed(), "table should not be closed") + assert_true(t:is_ipv6(), "table should be IPv6") + t:close() +end) + +test("create IPv6 table (wide16 algorithm)", function() + local t = lpm.new_ipv6("wide16") + assert_not_nil(t, "table should be created") + t:close() +end) + +test("create IPv6 table (stride8 algorithm)", function() + local t = lpm.new_ipv6("stride8") + assert_not_nil(t, "table should be created") + t:close() +end) + +-- ============================================================================ +-- Table Lifecycle Tests +-- ============================================================================ + +test("close table", function() + local t = lpm.new_ipv4() + assert_false(t:is_closed(), "table should not be closed initially") + t:close() + assert_true(t:is_closed(), "table should be closed after close()") +end) + +test("double close is safe", function() + local t = lpm.new_ipv4() + t:close() + t:close() -- Should not error + assert_true(t:is_closed(), "table should remain closed") +end) + +test("operations on closed table fail", function() + local t = lpm.new_ipv4() + t:close() + + assert_error(function() + t:insert("192.168.0.0/16", 100) + end, "insert on closed table should fail") + + assert_error(function() + t:lookup("192.168.1.1") + end, "lookup on closed table should fail") +end) + +test("tostring works", function() + local t = lpm.new_ipv4() + local s = tostring(t) + assert_true(s:find("IPv4") ~= nil, "tostring should mention IPv4") + t:close() + s = tostring(t) + assert_true(s:find("closed") ~= nil, "tostring should mention closed") +end) + +-- ============================================================================ +-- IPv4 Insert Tests +-- ============================================================================ + +test("insert IPv4 with CIDR string", function() + local t = lpm.new_ipv4() + local ok, err = t:insert("192.168.0.0/16", 100) + assert_true(ok, "insert should succeed") + t:close() +end) + +test("insert IPv4 with address + prefix_len", function() + local t = lpm.new_ipv4() + local ok, err = t:insert("10.0.0.0", 8, 200) + assert_true(ok, "insert should succeed") + t:close() +end) + +test("insert IPv4 with byte table", function() + local t = lpm.new_ipv4() + local ok, err = t:insert({172, 16, 0, 0}, 12, 300) + assert_true(ok, "insert should succeed") + t:close() +end) + +test("insert IPv4 default route", function() + local t = lpm.new_ipv4() + local ok, err = t:insert("0.0.0.0/0", 999) + assert_true(ok, "insert should succeed") + t:close() +end) + +test("insert IPv4 host route (/32)", function() + local t = lpm.new_ipv4() + local ok, err = t:insert("192.168.1.100/32", 500) + assert_true(ok, "insert should succeed") + t:close() +end) + +-- ============================================================================ +-- IPv4 Lookup Tests +-- ============================================================================ + +test("lookup IPv4 with string address", function() + local t = lpm.new_ipv4() + t:insert("192.168.0.0/16", 100) + + local nh = t:lookup("192.168.1.1") + assert_eq(nh, 100, "should find next hop") + t:close() +end) + +test("lookup IPv4 with byte table", function() + local t = lpm.new_ipv4() + t:insert("10.0.0.0/8", 200) + + local nh = t:lookup({10, 1, 2, 3}) + assert_eq(nh, 200, "should find next hop") + t:close() +end) + +test("lookup IPv4 no match returns nil", function() + local t = lpm.new_ipv4() + t:insert("192.168.0.0/16", 100) + + local nh = t:lookup("10.0.0.1") + assert_nil(nh, "should return nil for no match") + t:close() +end) + +test("lookup IPv4 longest prefix match", function() + local t = lpm.new_ipv4() + t:insert("10.0.0.0/8", 100) + t:insert("10.1.0.0/16", 200) + t:insert("10.1.2.0/24", 300) + + assert_eq(t:lookup("10.0.0.1"), 100, "should match /8") + assert_eq(t:lookup("10.1.0.1"), 200, "should match /16") + assert_eq(t:lookup("10.1.2.1"), 300, "should match /24") + t:close() +end) + +test("lookup IPv4 default route fallback", function() + local t = lpm.new_ipv4() + t:insert("0.0.0.0/0", 999) + t:insert("192.168.0.0/16", 100) + + assert_eq(t:lookup("192.168.1.1"), 100, "should match specific") + assert_eq(t:lookup("8.8.8.8"), 999, "should match default") + t:close() +end) + +-- ============================================================================ +-- IPv4 Delete Tests +-- ============================================================================ + +test("delete IPv4 with CIDR string", function() + local t = lpm.new_ipv4() + t:insert("192.168.0.0/16", 100) + + local ok, err = t:delete("192.168.0.0/16") + assert_true(ok, "delete should succeed") + + local nh = t:lookup("192.168.1.1") + assert_nil(nh, "should not find deleted route") + t:close() +end) + +test("delete IPv4 non-existent route is idempotent", function() + local t = lpm.new_ipv4() + + -- Delete is idempotent - deleting a non-existent route succeeds + local ok, err = t:delete("192.168.0.0/16") + assert_true(ok, "delete of non-existent route should succeed (idempotent)") + t:close() +end) + +test("delete IPv4 preserves other routes", function() + local t = lpm.new_ipv4() + t:insert("10.0.0.0/8", 100) + t:insert("192.168.0.0/16", 200) + + t:delete("10.0.0.0/8") + + assert_nil(t:lookup("10.1.1.1"), "deleted route should not match") + assert_eq(t:lookup("192.168.1.1"), 200, "other route should remain") + t:close() +end) + +-- ============================================================================ +-- IPv6 Insert and Lookup Tests +-- ============================================================================ + +test("insert and lookup IPv6 CIDR", function() + local t = lpm.new_ipv6() + local ok = t:insert("2001:db8::/32", 100) + assert_true(ok, "insert should succeed") + + local nh = t:lookup("2001:db8::1") + assert_eq(nh, 100, "should find next hop") + t:close() +end) + +test("insert and lookup IPv6 with address + prefix_len", function() + local t = lpm.new_ipv6() + local ok = t:insert("fe80::", 10, 200) + assert_true(ok, "insert should succeed") + + local nh = t:lookup("fe80::1") + assert_eq(nh, 200, "should find next hop") + t:close() +end) + +test("IPv6 longest prefix match", function() + local t = lpm.new_ipv6() + t:insert("2001:db8::/32", 100) + t:insert("2001:db8:1::/48", 200) + t:insert("2001:db8:1:2::/64", 300) + + assert_eq(t:lookup("2001:db8::1"), 100, "should match /32") + assert_eq(t:lookup("2001:db8:1::1"), 200, "should match /48") + assert_eq(t:lookup("2001:db8:1:2::1"), 300, "should match /64") + t:close() +end) + +test("IPv6 no match returns nil", function() + local t = lpm.new_ipv6() + t:insert("2001:db8::/32", 100) + + local nh = t:lookup("2001:db9::1") + assert_nil(nh, "should return nil for no match") + t:close() +end) + +test("IPv6 default route", function() + local t = lpm.new_ipv6() + t:insert("::/0", 999) + + local nh = t:lookup("2001:db8::1") + assert_eq(nh, 999, "should match default route") + t:close() +end) + +test("IPv6 host route (/128)", function() + local t = lpm.new_ipv6() + t:insert("2001:db8::1/128", 500) + + assert_eq(t:lookup("2001:db8::1"), 500, "should match exact host") + assert_nil(t:lookup("2001:db8::2"), "should not match different host") + t:close() +end) + +-- ============================================================================ +-- Batch Lookup Tests +-- ============================================================================ + +test("batch lookup IPv4", function() + local t = lpm.new_ipv4() + t:insert("10.0.0.0/8", 100) + t:insert("192.168.0.0/16", 200) + + local addrs = {"10.1.1.1", "192.168.1.1", "8.8.8.8"} + local results = t:lookup_batch(addrs) + + assert_eq(results[1], 100, "first should match /8") + assert_eq(results[2], 200, "second should match /16") + assert_nil(results[3], "third should not match") + t:close() +end) + +test("batch lookup IPv4 with byte tables", function() + local t = lpm.new_ipv4() + t:insert("10.0.0.0/8", 100) + + local addrs = {{10, 1, 1, 1}, {10, 2, 2, 2}} + local results = t:lookup_batch(addrs) + + assert_eq(results[1], 100, "first should match") + assert_eq(results[2], 100, "second should match") + t:close() +end) + +test("batch lookup IPv6", function() + local t = lpm.new_ipv6() + t:insert("2001:db8::/32", 100) + t:insert("fe80::/10", 200) + + local addrs = {"2001:db8::1", "fe80::1", "::1"} + local results = t:lookup_batch(addrs) + + assert_eq(results[1], 100, "first should match") + assert_eq(results[2], 200, "second should match") + assert_nil(results[3], "third should not match") + t:close() +end) + +test("batch lookup empty table", function() + local t = lpm.new_ipv4() + local results = t:lookup_batch({}) + assert_eq(#results, 0, "empty batch should return empty table") + t:close() +end) + +test("batch lookup large batch", function() + local t = lpm.new_ipv4() + t:insert("0.0.0.0/0", 100) + + -- Create 1000 addresses + local addrs = {} + for i = 1, 1000 do + addrs[i] = string.format("%d.%d.%d.%d", + i % 256, (i // 256) % 256, (i // 65536) % 256, 0) + end + + local results = t:lookup_batch(addrs) + assert_eq(#results, 1000, "should return 1000 results") + + for i = 1, 1000 do + assert_eq(results[i], 100, string.format("result %d should match", i)) + end + t:close() +end) + +-- ============================================================================ +-- Functional API Tests +-- ============================================================================ + +test("functional API insert", function() + local t = lpm.new_ipv4() + local ok = lpm.insert(t, "192.168.0.0/16", 100) + assert_true(ok, "functional insert should work") + t:close() +end) + +test("functional API lookup", function() + local t = lpm.new_ipv4() + lpm.insert(t, "192.168.0.0/16", 100) + local nh = lpm.lookup(t, "192.168.1.1") + assert_eq(nh, 100, "functional lookup should work") + t:close() +end) + +test("functional API delete", function() + local t = lpm.new_ipv4() + lpm.insert(t, "192.168.0.0/16", 100) + local ok = lpm.delete(t, "192.168.0.0/16") + assert_true(ok, "functional delete should work") + t:close() +end) + +test("functional API close and is_closed", function() + local t = lpm.new_ipv4() + assert_false(lpm.is_closed(t), "should not be closed") + lpm.close(t) + assert_true(lpm.is_closed(t), "should be closed") +end) + +-- ============================================================================ +-- Error Handling Tests +-- ============================================================================ + +test("insert with invalid CIDR fails", function() + local t = lpm.new_ipv4() + local ok, err = t:insert("invalid/cidr", 100) + assert_nil(ok, "should fail") + assert_not_nil(err, "should have error message") + t:close() +end) + +test("insert with invalid prefix length fails", function() + local t = lpm.new_ipv4() + local ok, err = t:insert("192.168.0.0/33", 100) + assert_nil(ok, "should fail for /33") + t:close() +end) + +test("insert with negative prefix length fails", function() + local t = lpm.new_ipv4() + local ok, err = t:insert("192.168.0.0", -1, 100) + assert_nil(ok, "should fail for negative prefix") + t:close() +end) + +test("insert with invalid next_hop fails", function() + local t = lpm.new_ipv4() + local ok, err = t:insert("192.168.0.0/16", -1) + assert_nil(ok, "should fail for negative next_hop") + t:close() +end) + +test("lookup with invalid address type fails", function() + local t = lpm.new_ipv4() + assert_error(function() + t:lookup(12345) -- number instead of string + end, "should fail for invalid type") + t:close() +end) + +test("IPv4 lookup with IPv6 address fails", function() + local t = lpm.new_ipv4() + t:insert("192.168.0.0/16", 100) + assert_error(function() + t:lookup("2001:db8::1") + end, "should fail for IPv6 address in IPv4 table") + t:close() +end) + +-- ============================================================================ +-- Edge Cases +-- ============================================================================ + +test("overlapping prefixes", function() + local t = lpm.new_ipv4() + t:insert("10.0.0.0/8", 1) + t:insert("10.0.0.0/16", 2) + t:insert("10.0.0.0/24", 3) + + assert_eq(t:lookup("10.0.0.1"), 3, "most specific should win") + assert_eq(t:lookup("10.0.1.1"), 2, "next specific should win") + assert_eq(t:lookup("10.1.0.1"), 1, "least specific should win") + t:close() +end) + +test("update route (re-insert)", function() + local t = lpm.new_ipv4() + t:insert("192.168.0.0/16", 100) + assert_eq(t:lookup("192.168.1.1"), 100) + + t:insert("192.168.0.0/16", 200) -- Update + assert_eq(t:lookup("192.168.1.1"), 200, "should have updated value") + t:close() +end) + +test("many routes", function() + local t = lpm.new_ipv4() + + -- Insert 1000 routes + for i = 0, 999 do + local a = (i // 256) % 256 + local b = i % 256 + t:insert(string.format("10.%d.%d.0/24", a, b), i) + end + + -- Verify some + assert_eq(t:lookup("10.0.0.1"), 0) + assert_eq(t:lookup("10.0.255.1"), 255) + assert_eq(t:lookup("10.3.231.1"), 999) + t:close() +end) + +test("binary string address", function() + local t = lpm.new_ipv4() + t:insert("192.168.0.0/16", 100) + + -- 4-byte binary string for 192.168.1.1 + local addr = string.char(192, 168, 1, 1) + local nh = t:lookup(addr) + assert_eq(nh, 100, "binary string lookup should work") + t:close() +end) + +-- ============================================================================ +-- Algorithm Comparison Tests +-- ============================================================================ + +test("dir24 and stride8 give same results", function() + local t1 = lpm.new_ipv4("dir24") + local t2 = lpm.new_ipv4("stride8") + + -- Same routes + local routes = { + {"10.0.0.0/8", 100}, + {"192.168.0.0/16", 200}, + {"172.16.0.0/12", 300}, + } + + for _, r in ipairs(routes) do + t1:insert(r[1], r[2]) + t2:insert(r[1], r[2]) + end + + -- Same lookups + local addrs = {"10.1.1.1", "192.168.1.1", "172.17.1.1", "8.8.8.8"} + for _, addr in ipairs(addrs) do + assert_eq(t1:lookup(addr), t2:lookup(addr), + string.format("results should match for %s", addr)) + end + + t1:close() + t2:close() +end) + +test("wide16 and stride8 give same results for IPv6", function() + local t1 = lpm.new_ipv6("wide16") + local t2 = lpm.new_ipv6("stride8") + + local routes = { + {"2001:db8::/32", 100}, + {"fe80::/10", 200}, + } + + for _, r in ipairs(routes) do + t1:insert(r[1], r[2]) + t2:insert(r[1], r[2]) + end + + local addrs = {"2001:db8::1", "fe80::1", "::1"} + for _, addr in ipairs(addrs) do + assert_eq(t1:lookup(addr), t2:lookup(addr), + string.format("results should match for %s", addr)) + end + + t1:close() + t2:close() +end) + +-- ============================================================================ +-- Run All Tests +-- ============================================================================ + +print("liblpm Lua bindings test suite") +print("Version: " .. (lpm._VERSION or "unknown")) +print("Library: " .. (lpm.version() or "unknown")) +print("") + +run_tests() diff --git a/docker/Dockerfile.lua b/docker/Dockerfile.lua new file mode 100644 index 0000000..c4d69ab --- /dev/null +++ b/docker/Dockerfile.lua @@ -0,0 +1,129 @@ +# Lua bindings container for liblpm +# Builds and tests Lua 5.4 wrapper with latest compilers + +FROM ubuntu:25.10 + +LABEL maintainer="Murilo Chianfa " +LABEL description="liblpm Lua binding" + +ENV DEBIAN_FRONTEND=noninteractive + +# Install development environment +RUN apt-get update && apt-get install -y --no-install-recommends \ + # Core build tools + build-essential \ + gcc-15 \ + g++-15 \ + # Build systems + cmake \ + ninja-build \ + make \ + git \ + pkg-config \ + # Libraries + libc6-dev \ + libnuma-dev \ + # Lua development + lua5.4 \ + liblua5.4-dev \ + # Optional: LuaRocks for package management + luarocks \ + # Python for scripts + python3 \ + && rm -rf /var/lib/apt/lists/* + +# Set compiler alternatives +RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 100 && \ + update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-15 100 && \ + update-alternatives --install /usr/bin/cc cc /usr/bin/gcc 100 && \ + update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++ 100 + +WORKDIR /build + +# Copy source code +COPY . /build/ + +# Initialize git submodules +RUN git config --global --add safe.directory /build && \ + if [ -f .gitmodules ]; then git submodule update --init --recursive; fi + +# Build liblpm with Lua wrapper +RUN mkdir -p build && cd build && \ + cmake \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_LUA_WRAPPER=ON \ + -DBUILD_TESTS=ON \ + -DENABLE_NATIVE_ARCH=OFF \ + -GNinja \ + .. && \ + ninja + +# Install liblpm system-wide (needed for LD_PRELOAD workaround) +RUN cd build && ninja install && ldconfig + +# Create test script using heredoc +# Note: Tests run with LD_PRELOAD to handle ifunc resolver issues when +# loading the module via dlopen +RUN cat > /build/run_lua_tests.sh << 'ENDSCRIPT' +#!/bin/bash +set -e + +echo "=== liblpm Lua Bindings Test Suite ===" +echo "" + +echo "=== System Information ===" +echo "Lua version: $(lua5.4 -v 2>&1)" +echo "GCC version: $(gcc --version | head -1)" +echo "" + +# Set up environment for tests +export LD_PRELOAD=/usr/local/lib/liblpm.so +export LUA_CPATH="/usr/local/lib/lua/5.4/?.so;;" + +echo "liblpm version: $(lua5.4 -e "print(require('liblpm').version())")" +echo "" + +echo "=== Running Lua Unit Tests (54 tests) ===" +lua5.4 /build/bindings/lua/tests/test_lpm.lua + +echo "" +echo "=== Running Lua Examples ===" +echo "" +echo "--- Basic Example ---" +lua5.4 /build/bindings/lua/examples/basic_example.lua + +echo "" +echo "--- IPv6 Example ---" +lua5.4 /build/bindings/lua/examples/ipv6_example.lua + +echo "" +echo "--- Batch Example ---" +lua5.4 /build/bindings/lua/examples/batch_example.lua + +echo "" +echo "=== Lua Bindings Test Summary ===" +echo "All tests passed!" +echo " - 54 unit tests" +echo " - Basic example" +echo " - IPv6 example" +echo " - Batch example" +ENDSCRIPT +RUN chmod +x /build/run_lua_tests.sh + +# Export volume for built artifacts +VOLUME ["/build/artifacts"] + +# Export built libraries +RUN mkdir -p /build/artifacts && \ + cp build/liblpm.so* /build/artifacts/ 2>/dev/null || true && \ + cp build/liblpm.a /build/artifacts/ 2>/dev/null || true && \ + cp build/bindings/lua/liblpm.so /build/artifacts/liblpm_lua.so 2>/dev/null || true + +# Default command runs Lua tests +CMD ["/build/run_lua_tests.sh"] + +# Usage: +# Build: docker build -f docker/Dockerfile.lua -t liblpm-lua . +# Run tests: docker run --rm liblpm-lua +# Interactive: docker run -it --rm liblpm-lua bash +# Extract artifacts: docker run --rm -v "$PWD/artifacts:/build/artifacts" liblpm-lua diff --git a/docker/README.md b/docker/README.md index c9edc06..a080293 100644 --- a/docker/README.md +++ b/docker/README.md @@ -12,6 +12,7 @@ Quick reference for liblpm Docker images. | `liblpm-fuzz` | AFL++ fuzzing | Security testing | | `liblpm-cpp` | C++ bindings | C++ wrapper testing | | `liblpm-go` | Go bindings | Go wrapper testing | +| `liblpm-lua` | Lua bindings | Lua wrapper testing | | `liblpm-benchmark` | DPDK benchmarking | Performance comparison | | `liblpm-deb` | DEB package builder | Building Debian/Ubuntu packages | | `liblpm-rpm` | RPM package builder | Building RHEL/Fedora/Rocky packages | @@ -79,6 +80,9 @@ docker run --rm liblpm-cpp # Test Go bindings docker run --rm liblpm-go + +# Test Lua bindings +docker run --rm liblpm-lua ``` ### Benchmarking @@ -198,6 +202,34 @@ Go bindings with cgo support. docker run --rm liblpm-go ``` +### liblpm-lua + +Lua 5.4 bindings with native C module. + +**Size:** ~400MB + +**Features:** +- Lua 5.4 support +- Native C module via Lua C API +- Object-oriented and functional APIs +- Batch lookup operations +- Automatic memory management via __gc metamethod +- Comprehensive test suite (54 tests) + +```bash +# Run tests +docker run --rm liblpm-lua + +# Interactive development +docker run -it --rm liblpm-lua bash + +# Run examples +docker run --rm liblpm-lua lua5.4 /build/bindings/lua/examples/basic_example.lua + +# Run specific example +docker run --rm liblpm-lua bash -c "LD_PRELOAD=/usr/local/lib/liblpm.so lua5.4 /build/bindings/lua/examples/ipv6_example.lua" +``` + ### liblpm-benchmark DPDK 24.11 integration for performance comparison. @@ -249,6 +281,7 @@ Approximate sizes (uncompressed): | liblpm-fuzz | ~1GB | | liblpm-cpp | ~800MB | | liblpm-go | ~600MB | +| liblpm-lua | ~400MB | | liblpm-benchmark | ~1.5GB | | liblpm-deb | ~400MB | | liblpm-rpm | ~500MB | diff --git a/scripts/docker-build.sh b/scripts/docker-build.sh index 80f9949..ac05d5e 100755 --- a/scripts/docker-build.sh +++ b/scripts/docker-build.sh @@ -51,6 +51,7 @@ Available Images: fuzz - AFL++ fuzzing environment cpp - C++ bindings go - Go bindings + lua - Lua bindings benchmark - DPDK benchmark environment all - Build all images (default) @@ -114,7 +115,7 @@ while [[ $# -gt 0 ]]; do VERBOSE="--progress=plain" shift ;; - base|dev|test|fuzz|cpp|go|benchmark|all) + base|dev|test|fuzz|cpp|go|lua|benchmark|all) IMAGES+=("$1") shift ;; @@ -217,6 +218,9 @@ build_images() { go) build_image "go" "${DOCKER_DIR}/Dockerfile.go" ;; + lua) + build_image "lua" "${DOCKER_DIR}/Dockerfile.lua" + ;; benchmark) build_image "benchmark" "${DOCKER_DIR}/Dockerfile.benchmark" ;; @@ -227,6 +231,7 @@ build_images() { build_image "fuzz" "${DOCKER_DIR}/Dockerfile.fuzz" build_image "cpp" "${DOCKER_DIR}/Dockerfile.cpp" build_image "go" "${DOCKER_DIR}/Dockerfile.go" + build_image "lua" "${DOCKER_DIR}/Dockerfile.lua" build_image "benchmark" "${DOCKER_DIR}/Dockerfile.benchmark" ;; *)