Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Proxelar is a man-in-the-middle proxy written in Rust. It intercepts, inspects, and optionally modifies HTTP and HTTPS traffic flowing between a client and a server.

What can it do?

  • Inspect traffic — see every request and response in real time, including headers and bodies
  • Intercept HTTPS — automatic CA certificate generation and per-host certificate minting
  • Modify traffic with Lua scripts — write on_request and on_response hooks to transform, block, or mock traffic at runtime
  • Forward and reverse proxy — use as a system proxy (forward) or put it in front of your service (reverse)
  • Three interfaces — interactive TUI, plain terminal output, or web GUI

Architecture

Proxelar is built as a three-crate Rust workspace:

  • proxelar-cli — the CLI binary with three interface modes
  • proxyapi — the core proxy engine, usable as a standalone library
  • proxyapi_models — shared request/response data types

The proxy engine is built on hyper 1.x, rustls 0.23, and tokio. HTTPS interception uses OpenSSL for certificate generation and rustls for TLS termination. Lua scripting is powered by mlua with a vendored Lua 5.4.

Installation

From crates.io

cargo install proxelar

This builds and installs the proxelar binary. Lua 5.4 and OpenSSL are vendored and compiled from source, so no system dependencies are required beyond a Rust toolchain.

From source

git clone https://github.com/emanuele-em/proxelar.git
cd proxelar
cargo build --release

The binary is at target/release/proxelar.

Without Lua scripting

If you don’t need scripting and want a smaller build:

cargo install proxelar --no-default-features

Requirements

  • Rust 1.94 or later
  • Works on Linux, macOS, and Windows

Quick Start

1. Start the proxy

proxelar

This starts Proxelar in forward proxy mode with the interactive TUI on 127.0.0.1:8080.

2. Install the CA certificate

To inspect HTTPS traffic, your system needs to trust Proxelar’s CA certificate. The easiest way:

  1. Configure your browser or system proxy to 127.0.0.1:8080
  2. Visit http://proxel.ar through the proxy
  3. Follow the platform-specific installation instructions on the page

Alternatively, manually install ~/.proxelar/proxelar-ca.pem.

3. Browse through the proxy

Configure your system or browser proxy to 127.0.0.1:8080. All HTTP and HTTPS traffic now flows through Proxelar and appears in the TUI.

Use the keyboard to navigate:

KeyAction
j / k / arrowsNavigate requests
EnterToggle detail panel
TabSwitch between Request / Response
/Filter
EscClose panel / clear filter
g / GJump to top / bottom
cClear all requests
q / Ctrl+CQuit

4. Try a Lua script

Create a file called script.lua:

function on_request(request)
    request.headers["X-Proxied-By"] = "proxelar"
    return request
end

Run Proxelar with the script:

proxelar --script script.lua

Every request passing through the proxy now has the X-Proxied-By header injected.

CA Certificate

Proxelar intercepts HTTPS traffic by generating a local Certificate Authority (CA) and minting per-host leaf certificates on the fly. For this to work, your system must trust the Proxelar CA.

Automatic generation

On first run, Proxelar generates a 4096-bit RSA CA certificate and private key in ~/.proxelar/:

  • ~/.proxelar/proxelar-ca.pem — CA certificate
  • ~/.proxelar/proxelar-ca-key.pem — CA private key (mode 0600)

If these files already exist, they are reused.

Certificate download server

The easiest way to install the CA is through the built-in download server. With the proxy running, visit:

http://proxel.ar

This page provides:

  • Direct download links for PEM and DER formats
  • Platform-specific installation instructions for macOS, Linux, Windows, iOS, and Android

Manual installation

macOS

sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain \
  ~/.proxelar/proxelar-ca.pem

Linux (Debian/Ubuntu)

sudo cp ~/.proxelar/proxelar-ca.pem /usr/local/share/ca-certificates/proxelar.crt
sudo update-ca-certificates

Linux (Fedora/RHEL)

sudo cp ~/.proxelar/proxelar-ca.pem /etc/pki/ca-trust/source/anchors/proxelar.pem
sudo update-ca-trust

Windows

certutil -addstore -f "ROOT" %USERPROFILE%\.proxelar\proxelar-ca.pem

Firefox

Firefox uses its own certificate store. Go to Settings > Privacy & Security > Certificates > View Certificates > Import, and select ~/.proxelar/proxelar-ca.pem.

Custom CA directory

Use --ca-dir to store the CA files in a different location:

proxelar --ca-dir /path/to/certs

Per-host certificate caching

Leaf certificates are cached in memory (up to 1,000 hosts). Repeated connections to the same host reuse the cached certificate instead of generating a new one.

Forward Proxy

Forward proxy is the default mode. Clients send their traffic to Proxelar, which forwards it to the destination server. This is the standard setup for inspecting browser or application traffic.

Usage

proxelar

Configure your client (browser, curl, application) to use 127.0.0.1:8080 as the HTTP/HTTPS proxy.

How it works

  1. The client sends a request to the proxy
  2. For HTTPS, the client sends a CONNECT request. Proxelar upgrades the connection and detects the protocol:
    • TLS ClientHello — generates a leaf certificate for the target host, terminates TLS, and inspects the decrypted traffic
    • Plain HTTP (e.g., GET prefix) — serves the stream directly
    • Unknown protocol — tunnels the raw TCP connection without inspection
  3. For plain HTTP, the request is forwarded directly
  4. Lua on_request / on_response hooks run at each step (if a script is loaded)

Examples

# Start forward proxy on default port
proxelar

# Custom port and bind address
proxelar -p 9090 -b 0.0.0.0

# With terminal output instead of TUI
proxelar -i terminal

# With a Lua script
proxelar --script block_ads.lua

# Test with curl
curl -x http://127.0.0.1:8080 http://httpbin.org/get
curl -x http://127.0.0.1:8080 https://httpbin.org/get

Reverse Proxy

In reverse proxy mode, Proxelar sits in front of a backend service. Clients connect to Proxelar directly (without proxy configuration), and all requests are forwarded to the specified target.

This is useful for debugging local APIs, injecting headers, mocking endpoints, or testing how your frontend handles modified responses.

Usage

proxelar -m reverse --target http://localhost:3000

Clients connect to http://127.0.0.1:8080 and Proxelar forwards everything to http://localhost:3000.

How it works

  1. The client sends a request to 127.0.0.1:8080
  2. Proxelar rewrites the URI to the target (preserving path and query)
  3. The Host header is updated to match the target
  4. Lua on_request / on_response hooks run (if a script is loaded)
  5. The response is returned to the client

Examples

# Reverse proxy to a local service
proxelar -m reverse --target http://localhost:3000

# Custom port (clients connect to 4000, forwarded to 3000)
proxelar -m reverse --target http://localhost:3000 -p 4000

# With a Lua script that injects auth headers
proxelar -m reverse --target http://localhost:3000 --script auth_dev.lua

# With web GUI
proxelar -m reverse --target http://localhost:3000 -i gui

Common use cases with scripting

Inject authentication

function on_request(request)
    request.headers["Authorization"] = "Bearer dev-token-12345"
    return request
end

Add security headers

function on_response(request, response)
    response.headers["Strict-Transport-Security"] = "max-age=31536000"
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "DENY"
    return response
end

Simulate errors

function on_request(request)
    if string.find(request.url, "/api/payments") then
        return {
            status = 500,
            headers = { ["Content-Type"] = "application/json" },
            body = '{"error": "Internal Server Error"}',
        }
    end
end

Lua Scripting

Proxelar supports Lua scripts that hook into the request/response lifecycle. You can modify headers, rewrite URLs, block requests, mock API responses, transform bodies, and more — all without recompiling or changing your application.

Running a script

proxelar --script my_script.lua

The script is loaded once at startup. It applies to all traffic flowing through the proxy, in both forward and reverse modes.

Writing a script

A script defines one or both of these global functions:

function on_request(request)
    -- Called before forwarding the request to the upstream server.
    -- Modify and return the request, return a response to short-circuit,
    -- or return nil to pass through unchanged.
end

function on_response(request, response)
    -- Called before returning the response to the client.
    -- Modify and return the response, or return nil to pass through unchanged.
end

Both functions are optional. If a function is not defined, traffic passes through unchanged.

Request hook

on_request receives a request table and can:

  1. Modify and forward — change headers, URL, method, or body, then return the request table
  2. Short-circuit — return a response table (with status, headers, body) to respond immediately without contacting the upstream server
  3. Pass through — return nil (or nothing) to forward the request unchanged
-- Modify and forward
function on_request(request)
    request.headers["X-Custom"] = "value"
    return request
end

-- Short-circuit with a response
function on_request(request)
    if string.find(request.url, "blocked%.com") then
        return { status = 403, headers = {}, body = "Blocked" }
    end
end

-- Pass through (implicit nil return)
function on_request(request)
    print(request.method .. " " .. request.url)
end

Response hook

on_response receives both the request (for context) and the response. It can modify and return the response, or return nil to pass through.

function on_response(request, response)
    response.headers["X-Proxy"] = "proxelar"
    return response
end

Error handling

Script errors are caught, logged, and the request passes through unchanged. A buggy script can never crash the proxy. Check the log output (set RUST_LOG=debug for details) to see script errors.

Feature flag

Lua scripting is behind the scripting feature flag, enabled by default. To build without it:

cargo install proxelar --no-default-features

API Reference

Request table

The on_request function receives a table with these fields:

FieldTypeDescription
methodstringHTTP method ("GET", "POST", "PUT", "DELETE", etc.)
urlstringFull request URL ("https://example.com/path?q=1")
headerstableRequest headers (see Headers below)
bodystringRequest body (may contain binary data, empty string for GET/HEAD)

All fields are readable and writable. Modify them in place and return the table to forward the modified request.

Response table

The on_response function receives two arguments:

  1. request — a table with method and url fields (for context)
  2. response — a table with these fields:
FieldTypeDescription
statusnumberHTTP status code (200, 404, 500, etc.)
headerstableResponse headers
bodystringResponse body

Short-circuit response

To respond immediately without contacting the upstream server, return a table with a status field from on_request:

return {
    status = 403,
    headers = { ["Content-Type"] = "text/plain" },
    body = "Forbidden",
}

The presence of the status field is what distinguishes a response from a modified request.

Headers

Headers are Lua tables mapping lowercase header names to values.

Single-value headers are plain strings:

request.headers["content-type"]     -- "application/json"
request.headers["authorization"]    -- "Bearer token123"

Multi-value headers (like Set-Cookie) are arrays:

response.headers["set-cookie"]      -- {"session=abc", "lang=en"}

When setting headers, both forms are accepted:

-- Single value (most common)
request.headers["x-custom"] = "value"

-- Multiple values
response.headers["set-cookie"] = {"a=1", "b=2"}

-- Remove a header
request.headers["cookie"] = nil

Return values

on_request

ReturnEffect
Request tableForward the (modified) request to upstream
Response table (has status)Short-circuit — return this response directly
nil (or no return)Pass through unchanged

on_response

ReturnEffect
Response tableReturn the (modified) response to the client
nil (or no return)Pass through unchanged

Available Lua standard libraries

Scripts run in a standard Lua 5.4 environment with access to:

  • string — pattern matching, formatting, manipulation
  • table — array/table operations
  • math — mathematical functions
  • os.date(), os.time(), os.clock() — time functions
  • print() — output to proxy stdout
  • tostring(), tonumber(), type() — type conversion

Script Examples

All examples below are complete, working scripts. They are also available in the examples/scripts/ directory.

Add headers to requests

function on_request(request)
    request.headers["X-Forwarded-By"] = "proxelar"
    request.headers["X-Request-Time"] = os.date("%Y-%m-%dT%H:%M:%S")
    return request
end

Block domains

local blocked = {
    "ads%.example%.com",
    "tracker%.example%.com",
    "analytics%.bad%.com",
}

function on_request(request)
    for _, pattern in ipairs(blocked) do
        if string.find(request.url, pattern) then
            return {
                status = 403,
                headers = { ["Content-Type"] = "text/plain" },
                body = "Blocked by Proxelar: " .. request.url,
            }
        end
    end
end

Mock API endpoints

function on_request(request)
    if request.method == "GET" and string.find(request.url, "/api/user/me") then
        return {
            status = 200,
            headers = { ["Content-Type"] = "application/json" },
            body = '{"id": 1, "name": "Test User", "email": "test@example.com"}',
        }
    end
end

Redirect requests to a different host

function on_request(request)
    request.url = string.gsub(request.url, "old%-api%.example%.com", "new-api.example.com")
    return request
end

Inject CORS headers

function on_response(request, response)
    response.headers["Access-Control-Allow-Origin"] = "*"
    response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
    response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
    return response
end

Log traffic to stdout

function on_request(request)
    print(string.format("[REQ] %s %s", request.method, request.url))
end

function on_response(request, response)
    local ct = response.headers["content-type"] or "unknown"
    local size = #response.body
    print(string.format("[RES] %s %s -> %d (%s, %d bytes)",
        request.method, request.url, response.status, ct, size))
end

Inject authentication

local TOKEN = "Bearer my-dev-token-12345"

function on_request(request)
    if string.find(request.url, "api%.example%.com") then
        request.headers["Authorization"] = TOKEN
    end
    return request
end

Modify JSON response bodies

function on_response(request, response)
    local ct = response.headers["content-type"] or ""
    if not string.find(ct, "application/json") then return end

    if string.sub(response.body, 1, 1) == "{" then
        response.body = '{"proxied":true,' .. string.sub(response.body, 2)
    end
    return response
end

Inject a banner into HTML pages

function on_response(request, response)
    local ct = response.headers["content-type"] or ""
    if not string.find(ct, "text/html") then return end

    local banner = '<div style="position:fixed;top:0;left:0;right:0;'
        .. 'background:#ff6b35;color:white;text-align:center;'
        .. 'padding:4px;z-index:99999;font-size:12px;">'
        .. 'Proxied by Proxelar</div>'

    response.body = string.gsub(response.body, "<body>", "<body>" .. banner, 1)
    return response
end

Only allow GET and HEAD

function on_request(request)
    if request.method ~= "GET" and request.method ~= "HEAD" then
        return {
            status = 405,
            headers = {
                ["Content-Type"] = "text/plain",
                ["Allow"] = "GET, HEAD",
            },
            body = "Method " .. request.method .. " not allowed by proxy policy",
        }
    end
end

Strip tracking cookies

local tracking_cookies = { "fbp", "_ga", "_gid", "fr", "datr" }

function on_request(request)
    local cookie = request.headers["cookie"]
    if not cookie then return end

    local parts = {}
    for pair in string.gmatch(cookie, "([^;]+)") do
        pair = string.match(pair, "^%s*(.-)%s*$")
        local name = string.match(pair, "^([^=]+)")
        local dominated = false
        for _, tc in ipairs(tracking_cookies) do
            if name == tc then dominated = true; break end
        end
        if not dominated then table.insert(parts, pair) end
    end

    if #parts > 0 then
        request.headers["cookie"] = table.concat(parts, "; ")
    else
        request.headers["cookie"] = nil
    end
    return request
end

Modify POST request bodies

function on_request(request)
    if request.method ~= "POST" then return end
    local ct = request.headers["content-type"] or ""

    if string.find(ct, "application/json") and string.sub(request.body, 1, 1) == "{" then
        request.body = '{"injected_by":"proxelar",' .. string.sub(request.body, 2)
    end
    return request
end

CLI Reference

proxelar [OPTIONS]

Options

FlagShortDefaultDescription
--interface-ituiInterface mode: terminal, tui, or gui
--mode-mforwardProxy mode: forward or reverse
--port-p8080Port to listen on
--addr-b127.0.0.1Bind address
--target-tUpstream target URI (required for reverse mode)
--script-sPath to a Lua script for request/response hooks
--gui-port8081Web GUI port (only used with -i gui)
--ca-dir~/.proxelarDirectory for CA certificate and key files

Environment variables

VariableDescription
RUST_LOGControls log verbosity. Examples: debug, proxyapi=trace, warn

Examples

# Default: forward proxy with TUI
proxelar

# Terminal output on custom port
proxelar -i terminal -p 9090

# Web GUI accessible from the network
proxelar -i gui -b 0.0.0.0

# Reverse proxy with script
proxelar -m reverse --target http://localhost:3000 --script auth.lua

# Forward proxy with logging script
proxelar --script log_traffic.lua

Interfaces

Proxelar provides three interface modes, all showing the same live traffic data.

TUI (default)

proxelar
# or
proxelar -i tui

An interactive terminal interface built with ratatui. Shows a table of all captured requests with method, status, host, path, and response size.

Key bindings

KeyAction
j / k / / Navigate requests
EnterToggle detail panel
TabSwitch between Request and Response tabs
/Enter filter mode (search by method or URL)
EscClose detail panel or clear filter
g / GJump to first / last request
cClear all captured requests
q / Ctrl+CQuit

The detail panel shows the full request or response including headers and body.

Terminal

proxelar -i terminal

Prints each request/response as a colored line to stdout. Useful for quick inspection or when piping output to other tools.

Output includes timestamp, HTTP method (color-coded), URL, status code, and response size.

Web GUI

proxelar -i gui

Opens a web interface at http://127.0.0.1:8081 (configurable with --gui-port). Built with axum and WebSocket for real-time streaming.

Features:

  • Interactive request table with live updates
  • Filter by HTTP method or URL
  • Click a row to view full request/response detail
  • JSON pretty-printing in the detail view
  • Light and dark mode (follows system preference)

To make the web GUI accessible from other machines:

proxelar -i gui -b 0.0.0.0