Skip to content

Latest commit

 

History

History
972 lines (765 loc) · 35.5 KB

CONFIG.md

File metadata and controls

972 lines (765 loc) · 35.5 KB

Configuration Reference

tobaru uses YAML configuration files. Multiple configuration types can be combined in a single file or split across multiple files.

Table of Contents

Configuration Structure

A configuration file is a YAML array containing one or more entries. Each entry can be:

  • Server Config -- defines a listener with one or more forwarding targets
  • IP Group -- defines a reusable named set of IP ranges
# Server config (has 'address')
- address: "0.0.0.0:443"
  transport: tcp
  targets:
    - location: backend:443
      allowlist: 0.0.0.0/0

# IP group (has 'group')
- group: internal
  ip_masks:
    - 192.168.0.0/16
    - 10.0.0.0/8

Multiple config files can be passed on the command line and are merged together. This is useful for separating IP group definitions from server definitions.

Supported Formats

  • YAML: .yml or .yaml extension (recommended)
  • JSON: .json extension, with optional // single-line comments

Server Config

address: "0.0.0.0:8080"           # Bind address (required)

transport: tcp | udp               # Default: tcp

# TCP-specific settings (only when transport: tcp)
tcp_nodelay: true                  # Disable Nagle's algorithm (default: true)
tcp_keepalive: true | false | null | { idle_secs: int, interval_secs: int }

use_iptables: false                # Auto-configure iptables rules (default: false)

# One or more targets
target: TcpTargetConfig            # Single target
targets: [TcpTargetConfig]         # Multiple targets

Address Format

address: "0.0.0.0:8080"           # IPv4
address: "[::]:8080"              # IPv6

TCP Keepalive

Controls TCP keepalive probes on the server-side (client-facing) socket.

# Use defaults (idle: 300s, interval: 60s)
tcp_keepalive: true                # or omit the field

# Disable keepalive
tcp_keepalive: false
tcp_keepalive: null

# Custom values
tcp_keepalive:
  idle_secs: 120
  interval_secs: 30

iptables Integration

When use_iptables: true, tobaru automatically configures iptables/ip6tables rules to drop packets from IPs not in the allowlist at the kernel level, before they reach the application.

Requires root or CAP_NET_RAW + CAP_NET_ADMIN capabilities. Supported on Linux only.

# Clear all tobaru-managed iptables rules
sudo tobaru --clear-iptables-all

# Clear rules for specific config files only
sudo tobaru --clear-iptables-matching config.yaml

TCP Targets

Each TCP target defines a forwarding destination with access control and optional TLS.

targets:
  - allowlist: string | [string]   # IP masks or group names (required)
    location: string               # Single backend address (e.g., "backend:8080")
    locations: [string | object]   # Multiple backends (round-robin)
    server_tls: ServerTlsConfig    # Optional incoming TLS config
    tcp_nodelay: true              # Default: true
    tcp_keepalive: ...             # Same options as server-level (default idle: 120s, interval: 30s)

target (singular) is an alias for a single-element targets array.

Location Formats

Locations specify where to forward traffic. Each location is either a plain address string or an object with additional settings.

# Simple string address
location: backend:8080

# UNIX domain socket path
location:
  path: /run/app.sock

# Address with outgoing TLS
location:
  address: upstream.example.com:443
  client_tls:
    verify: true

# Multiple backends (round-robin load balancing)
locations:
  - backend1:8080
  - backend2:8080
  - backend3:8080

The address and addresses field names are accepted as aliases for location and locations.

Allowlist

Controls which source IPs may use this target. An IP outside this target's allowlist may still use another eligible target on the listener. Connections from IPs not matching any target's allowlist are rejected.

CIDRs are normalized to their network address: 192.0.2.123/26 means 192.0.2.64/26. Host bits are cleared without changing the prefix length, for IPv4 and IPv6 literals and named-group entries. Equivalent spellings count as the same network for duplicate checks. Bare addresses still match only that host. Generated IPv4 firewall rules cover the portion of each CIDR within the mapped IPv4 block (::ffff:0:0/96), including IPv6 prefixes that contain that entire block.

# Allow all IPv4
allowlist: 0.0.0.0/0

# Multiple CIDR ranges
allowlist:
  - 192.168.1.0/24
  - 10.0.0.0/8

# Specific addresses
allowlist:
  - 1.2.3.4
  - 2001:db8::1

# Named IP groups
allowlist:
  - internal
  - trusted

# Mix of literals and groups
allowlist:
  - internal
  - 203.0.113.0/24

Target Evaluation Order

TCP routing checks all applicable conditions before selecting a target. A TLS target must match the source IP, SNI, and ALPN offer. A raw forwarding target without server_tls accepts arbitrary bytes, including TLS. A plaintext HTTP target is not an opaque TLS fallback.

Eligible targets are ranked in this order:

  1. Longest matching source-IP prefix in the target's allowlist.
  2. TLS over raw forwarding at the same IP prefix length.
  3. Exact SNI over the deepest matching wildcard, then shallower wildcards, then any.
  4. An explicitly matched ALPN protocol over any.
  5. Earlier target in configuration order for remaining ties.

Passthrough and termination have equal priority. A more specific SNI pattern whose IP or ALPN conditions fail does not hide eligible broader patterns. When a target lists multiple IP prefixes or SNI patterns, its most specific matching entry is used. Repeated IP prefixes across TCP targets are allowed; duplicates within a single target's allowlist remain errors.

For example, a raw /32 overrides a TLS /0 for that source IP. At equal IP prefix lengths, a matching TLS catch-all wins over a raw catch-all. Configuration order does not override these specificity rules. UDP retains longest-prefix IP routing without TLS inspection.

IP specificity is measured within the client's address family. For IPv4 clients, all, ::/0, and 0.0.0.0/0 have equal specificity; the internal IPv4-mapped IPv6 representation does not give an IPv4 catch-all extra priority. IPv6 prefixes covering the entire mapped IPv4 range likewise have IPv4 /0 specificity.

targets:
  # Internal users get admin backend
  - location: admin-backend:8080
    allowlist: 10.0.0.0/8
    server_tls:
      mode: passthrough
      sni_hostnames: app.example.com

  # Everyone else gets public backend
  - location: public-backend:8080
    allowlist: 0.0.0.0/0
    server_tls:
      mode: passthrough
      sni_hostnames: app.example.com

Raw Catch-All

A location or locations target without server_tls is protocol-agnostic. It remains eligible when no TLS route matches, including when another TLS target allows the same source IP. Its position in the target list does not make it override a more specific eligible route.

targets:
  # Forward everything not selected by a more specific route, including TLS.
  - allowlist: 0.0.0.0/0
    location: /run/catch-all.sock

  - allowlist: 0.0.0.0/0
    location: /run/api.sock
    server_tls:
      mode: passthrough
      sni_hostnames: api.example.com

Here, TLS for api.example.com uses /run/api.sock; TLS for other names, connections without SNI, and non-TLS traffic use /run/catch-all.sock.

When inspection is necessary, the ClientHello read has a five-second deadline. Malformed, incomplete, or oversized ClientHellos may fall back to an eligible raw target, replaying every buffered byte unchanged. Server-first protocols may wait for this deadline. A raw target known to outrank every applicable TLS route can be selected immediately without inspection. TLS-looking bytes never fall back to a plaintext HTTP target.

Once a target is selected, TLS handshake/authentication errors and backend connection errors terminate the connection. They do not retry another target.

Access control: a broad raw target intentionally grants fallback access, including when a specific TLS target's IP or ALPN condition fails. A target's allowlist is not a global deny rule for its SNI hostname. Remove or narrow the raw fallback, and any broader TLS routes, when unmatched traffic must be rejected.

Server TLS

Configures TLS handling for incoming connections.

server_tls:
  mode: terminate | passthrough    # Default: terminate

  # SNI hostname matching
  sni_hostnames: string | [string] # Hostname patterns (see below)

  # ALPN protocol matching
  alpn_protocols: string | [string]

  # Certificate (required for terminate mode)
  cert: string                     # Path to certificate file
  key: string                      # Path to private key file

  # Client certificate authentication (terminate mode only)
  client_ca_certs: [string]        # Paths to CA certificate PEM files
  client_fingerprints: [string]    # SHA256 fingerprints

  # Deprecated
  optional: false                  # true is auto-migrated to two separate targets

TLS Modes

Passthrough -- reads the SNI and ALPN from the TLS ClientHello without decrypting. The raw TLS stream is forwarded transparently to the backend. No certificate or key is needed on the proxy.

Terminate (default) -- performs a full TLS handshake, decrypts the traffic, and can inspect HTTP content before forwarding. Requires cert and key.

Both modes can coexist on the same port for different SNI hostnames:

targets:
  # Passthrough for public API
  - location: api-backend:443
    allowlist: 0.0.0.0/0
    server_tls:
      mode: passthrough
      sni_hostnames: api.example.com

  # Terminate for admin panel
  - location: admin-backend:8080
    allowlist: 10.0.0.0/8
    server_tls:
      mode: terminate
      cert: admin.crt
      key: admin.key
      sni_hostnames: admin.example.com

SNI Hostname Patterns

sni_hostnames: example.com          # Exact match only
sni_hostnames: "*.example.com"      # Any subdomain, but NOT example.com itself
sni_hostnames: ".example.com"       # example.com AND all subdomains
sni_hostnames: any                  # Any SNI value
sni_hostnames: none                 # Connections with no SNI
sni_hostnames:                      # Multiple patterns
  - api.example.com
  - "*.cdn.example.com"
  - any
  - none

Among eligible TLS targets at the same IP prefix length, matching priority is exact match > deepest wildcard > shallower wildcard > any. All matching patterns are considered before applying the target ranking.

Hostnames are case-insensitive. Trailing dots are stripped.

TLS Handshake Timeout

TLS termination accepts an optional positive server_tls.handshake_timeout_secs. It bounds handshake completion after ClientHello routing, independently of the existing ClientHello read timeout. Omitted or null disables the additional deadline. It is invalid for passthrough, where the backend owns the handshake. Terminated TLS does not accept 0-RTT early data.

server_tls:
  cert: server.crt
  key: server.key
  handshake_timeout_secs: 10

ALPN Protocol Matching

In passthrough mode, ALPN protocols from the ClientHello are matched against the configured list. In terminate mode, the configured protocols are advertised in the ServerHello.

alpn_protocols: h2                 # Single protocol
alpn_protocols:                    # Multiple protocols
  - h2
  - http/1.1
alpn_protocols: any                # Match any nonempty ALPN offer
alpn_protocols: none               # Match only when no ALPN

any controls matching, not protocol negotiation. Termination advertises only explicit protocol names; any without explicit names negotiates no ALPN. any does not match an absent ALPN offer; use [any, none] to accept both. Passthrough leaves negotiation to the backend. HTTP actions dispatch negotiated h2 to HTTP/2 and otherwise retain the HTTP/1 session, including legacy custom ALPN names. Use [h2, http/1.1, none] to accept both standard protocols and clients without ALPN. any does not by itself negotiate HTTP/2.

For terminating HTTP targets, omitted ALPN is derived from http_protocols: [h2, http/1.1, none] by default, [http/1.1, none] for H1-only, or [h2] for H2-only. Explicit ALPN lists are preserved and rejected if they permit a disabled HTTP protocol. Custom names, any, and none require H1 enabled. Explicit empty lists or null retain the generic no-ALPN behavior and also require H1 enabled. Raw forwarding and TLS passthrough do not derive HTTP ALPN settings.

Client Certificate Authentication

Requires clients to present a valid certificate. Only available in terminate mode (TLS 1.3 sends client certificates inside the encrypted tunnel, so passthrough mode cannot inspect them).

Two methods are supported and can be combined:

  • client_ca_certs -- accept any client certificate that chains to one of the provided CA certificates
  • client_fingerprints -- accept client certificates matching specific SHA256 fingerprints

When both are configured, a certificate is accepted if it passes either check.

server_tls:
  mode: terminate
  cert: server.crt
  key: server.key
  sni_hostnames: secure.example.com

  # Accept any cert signed by this CA
  client_ca_certs:
    - /path/to/ca.crt

  # Also accept these specific self-signed certs
  client_fingerprints:
    - "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"
    - "1122334455667788990011223344556677889900112233445566778899001122"

Fingerprints require 32 bytes (64 hexadecimal digits), with optional colons or spaces. Invalid fingerprints are rejected when loading configuration.

# Generate a CA and client certificate
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
  -nodes -keyout ca.key -out ca.crt -days 365 -subj "/CN=MyCA"
openssl ecparam -genkey -name prime256v1 -out client.key
openssl req -new -key client.key -out client.csr -subj "/CN=Client"
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out client.crt -days 365

# Get a certificate's SHA256 fingerprint
openssl x509 -in client.crt -noout -fingerprint -sha256

Client TLS

Configures TLS for outgoing connections to backend servers. Set within a location object.

location:
  address: upstream.example.com:443
  client_tls:
    verify: true                   # Verify server cert via WebPKI (default: true)
    cert: string                   # Client certificate for mTLS
    key: string                    # Client private key for mTLS
    sni_hostname: string | null    # SNI to send (default: derived from address, null to disable)
    alpn_protocols: [string]       # ALPN protocols to negotiate
    server_fingerprints: [string]  # SHA256 fingerprints for cert pinning

Shorthand Forms

# Enable with defaults (verify: true)
client_tls: true

# Enable with verification disabled
client_tls: "no-verify"

# Full object form
client_tls:
  verify: true
  sni_hostname: custom.example.com
  server_fingerprints:
    - "AA:BB:CC:..."

client_tls cannot be used with passthrough mode (would cause TLS-in-TLS).

verify: false and no-verify disable WebPKI certificate verification. Without configured certificate pins, an active attacker can impersonate the backend; use verification or explicit SHA256 pins for untrusted networks.

Server Certificate Pinning

client_tls:
  server_fingerprints:
    - "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"
# Get a server's certificate fingerprint
openssl s_client -connect example.com:443 < /dev/null 2>/dev/null | openssl x509 -outform PEM > server.crt
openssl x509 -in server.crt -noout -fingerprint -sha256

HTTP Routing

HTTP path-based routing is available when TLS is terminated (or for plain HTTP). A target with http_paths and/or default_http_action is treated as an HTTP target.

target:
  allowlist: 0.0.0.0/0
  http_paths:
    /api/:
      http_action:
        type: forward
        addresses:
          - api-backend:8080
    /static/:
      http_action:
        type: serve-directory
        path: /var/www/static
  default_http_action:
    type: forward
    addresses:
      - default-backend:8080

HTTP Timeouts

Timeouts belong to the HTTP target and apply to all of its routes. On the existing H1-to-H1 session they are disabled when omitted or null; configured values must be positive seconds. Mixed plaintext protocol detection and HTTP/2 paths use finite defaults described below.

target:
  allowlist: 127.0.0.1/32
  http_timeouts:
    request_header_timeout_secs: 15
    response_header_timeout_secs: 30
    keepalive_idle_timeout_secs: 60
  default_http_action:
    type: forward
    location: 127.0.0.1:8080
  • The request-header deadline starts when HTTP handling begins for the first request, or when the first byte of a subsequent request is received. Partial header progress does not reset it.
  • The response-header deadline starts after upstream request headers are sent and lasts until final response headers arrive. Informational responses do not reset it. It includes any concurrent request upload and backend processing; choose a value suitable for those workloads or leave it disabled.
  • The keepalive idle deadline bounds waiting for the next request's first byte. Once data arrives, the request-header deadline applies instead.

Expiration closes the affected HTTP session. These are not body-transfer or whole-session deadlines; upgraded tunnels are not subject to them.

HTTP/2 Policy and Limits

http_protocols is an ingress allowlist, default [http1, http2]. It must be a nonempty list without duplicates; select [http1] or [http2] to disable the other protocol. The old http2.prior_knowledge option is no longer accepted; replace prior_knowledge: true with target-level http_protocols: [http2].

With both protocols enabled, plaintext connections are detected incrementally: PRI selects H2; the first mismatch selects H1. All consumed bytes, including any read-ahead from optional TLS detection, are replayed unchanged. The H2 library validates the remaining preface. Invalid prefaces never fall back to H1. This reserves PRI as an H2-only method on mixed plaintext listeners, while longer H1 methods such as PRINT still work. Detection runs only once per connection, not for each keepalive request. Single-protocol listeners skip detection entirely.

Detection uses one absolute deadline from entry into HTTP dispatch, including waiting for the first byte: http_timeouts.request_header_timeout_secs when set, otherwise http2.header_timeout_secs (10 seconds). Byte progress does not extend it. After selection, the existing handler's header/handshake deadlines apply.

TLS never sniffs HTTP bytes. Negotiated h2 selects H2; http/1.1, custom ALPN names, and no ALPN select H1, provided that protocol is enabled. Omitted TLS ALPN is derived as described above; explicit ALPN is authoritative. H2-only TLS requires h2 negotiation. Generic TLS target matching still precedes HTTP dispatch. Raw streams and TLS passthrough are unaffected. Upgrade-based h2c is not supported.

A forward action independently selects upstream_protocol: http1 | http2 (default http1). H2 egress uses prior knowledge over clear TCP/Unix sockets or requires negotiated h2 over TLS. Omit client ALPN to derive [h2], or configure exactly [h2]; incompatible explicit lists are rejected. No automatic fallback.

target:
  allowlist: 127.0.0.1/32
  http_protocols: [http1, http2]
  http2:
    max_concurrent_streams: 64
    max_connections: 256
    max_backend_connections: 256
    max_header_list_size: 65536
    header_timeout_secs: 10
    connect_timeout_secs: 10
    body_progress_timeout_secs: 30
    drain_timeout_secs: 5
  default_http_action:
    type: forward
    upstream_protocol: http2
    location:
      address: backend.example:443
      client_tls: true

Protocol policy and limit values shown are defaults. Enabling H2 validates local response statuses and headers for H2 compatibility, even when an explicit ALPN list restricts TLS to H1. Use [http1] for H1-only local response configurations. max_concurrent_streams is per ingress H2 connection (maximum 4096). max_connections bounds admitted H2 connections per compiled target/generation, after TLS negotiation. max_backend_connections initializes two separate target-global limits: active new-path exchanges and live new-path backend transports, including dialing, idle and draining connections. Both connection limits are capped at 65536. Each frontend additionally retains at most 16 H2 backend drivers across all actions. Admission refuses excess work rather than creating an unbounded queue. H1-to-H1 connections retain their existing policy.

Header limits are 1024 through 1048576 decoded bytes, with a fixed 128-field cap. Receive windows are 64 KiB per stream and 1 MiB per connection; forwarding chunks are at most 16 KiB. Up to 16 informational responses are forwarded. Encoded field-section bytes and pre-consumption response-section counts are also bounded. Trailer fields are validated independently of initial headers and header patches. Pseudoheaders and oversized trailer sections are rejected before conversion to ordinary fields, for both ingress requests and upstream H2 responses, using the pinned h2 fork fixes. Literal # fragments are rejected before URI normalization on both H2 ingress and H1-to-H2 forwarding, so the backend cannot receive a different path from the one used for routing. Encoded %23 remains permitted. These checks require no additional configuration flags.

For sections received over HTTP/2, max_header_list_size is an inclusive receive policy, counting each field's name, value, and 32-byte overhead. Valid trailer sections exactly at the limit are preserved in full; sections one byte over it are rejected, not truncated. HTTP/1 fields on translation paths retain their existing accounting: raw field-line length, including the colon and whitespace, plus 32 bytes. The configured policy is distinct from the H2 decoder's additional abuse limits, which can terminate the connection rather than only the stream.

All H2 timeouts must be positive and at most 86400 seconds. The connect deadline includes action-slot waiting, DNS/TCP/Unix setup, TLS and H2 handshake/readiness. The header deadline is absolute for incomplete frames/header blocks, not extended by trickling bytes; expiry closes the connection. Existing explicit request-header timeout overrides the ingress frame/header default. Generic pre-ALPN TLS handshake policy remains controlled by server_tls.handshake_timeout_secs.

Default response-header deadline is 30 seconds after upload completion, plus a 30-second wait for Expect's 100/final response. Explicit response-header timeout instead remains absolute from response processing, including concurrent upload and informational responses. Body inactivity is shared across upload and response; meaningful progress in either direction keeps a duplex exchange alive. Frontend idle means no active streams, default 60 seconds, overridden by explicit keepalive idle timeout. Reload/idle retirement sends GOAWAY and bounds each owner's drain; top-level reload does not synchronously join established TCP sessions.

H2 preserves the logical request scheme and authority independently of backend TLS/SNI. Host patches update outgoing authority. Repeated headers, DATA and trailers are preserved; H1 ingress retains its existing UTF-8 restriction. Cross-version translation rejects unsupported transfer-coding chains and framing-changing patches, and uses H1 chunking when trailers may follow. H2-to-H1 backend leases are exclusive and not cached. H2 backend reuse is frontend/action-scoped, without cross-action coalescing or automatic retries; round-robin advances per physical connection, not per stream.

H2-to-H1 requests use one canonical Host, Connection: close, no incoming Content-Length or TE field, and chunked framing whenever the upload is open. DATA bytes cannot supply their own chunk boundaries. Failed uploads are not completed with a synthesized terminal chunk, and their backend sockets are never reused. If an early response is already in progress, upload failure half-closes the backend write side so an EOF-dependent response can finish.

CONNECT, extended CONNECT/WebSocket, H3 and push are not supported on H2 paths. H1 upgrades directed to an H2-only action receive 501. Local H2 responses must be final statuses; HEAD/204/205/304 do not transmit payload. Custom reason phrases are H1-only. close intentionally closes the entire H2 connection and its siblings; ordinary local responses end only their stream.

Path Matching

Paths use raw longest-prefix matching, including the query string. Keys without a trailing / are not exact matches. A trailing / provides a subtree boundary; the bare mount also matches when it has no query string.

http_paths:
  /api/:     # matches /api, /api/, /api/users; not /apiculture or /api?x=1
  /health:   # matches /health, /health?x=1, and /healthcheck

Only the longest matching key is considered. If its required headers do not match, default_http_action is used, not a shorter configured prefix.

Required Request Headers

Each path entry can require specific HTTP headers to match. Header keys are case-insensitive.

http_paths:
  /:
    - required_request_headers:
        x-api-key: secret123
      http_action:
        type: forward
        addresses: [backend:8080]

    - required_request_headers:
        host: app.example.com
      http_action:
        type: forward
        addresses: [app-backend:8080]

When multiple entries exist under the same path, they are evaluated in order. The first match wins.

Host Header Routing

The host key in required_request_headers supports the same wildcard patterns as sni_hostnames. Port suffixes in the Host header (e.g., example.com:8080) are automatically stripped before matching.

http_paths:
  /:
    # Exact match
    - required_request_headers:
        host: app.example.com
      http_action:
        type: forward
        addresses: [app-backend:8080]

    # Wildcard subdomains
    - required_request_headers:
        host: "*.api.example.com"
      http_action:
        type: forward
        addresses: [api-backend:8080]

    # Base domain + all subdomains
    - required_request_headers:
        host: ".example.com"
      http_action:
        type: forward
        addresses: [default-backend:8080]

default_http_action:
  type: serve-message
  status_code: 404

When possible, prefer SNI-level routing (sni_hostnames) over Host header matching. SNI routing operates at the TLS layer before HTTP parsing, avoiding the overhead of decryption and request parsing. Host header routing is useful for plain HTTP, or when multiple virtual hosts share the same TLS certificate.

HTTP Actions

forward

Forwards the HTTP request to one or more backend servers. Supports connection reuse and header manipulation; WebSocket upgrades remain H1-to-H1 only.

http_action:
  type: forward
  upstream_protocol: http1        # http1 (default) or http2
  locations: [string | object]     # Backend address(es) -- round-robin if multiple
  replacement_path: string         # Rewrite the request path
  request_header_patch:            # Modify request headers
    default_headers:               #   Add if not already present
      X-Forwarded-For: "..."
    overwrite_headers:             #   Set unconditionally
      Host: backend.internal
    remove_headers:                #   Remove
      - X-Debug
  response_header_patch:           # Modify response headers (same structure)
    overwrite_headers:
      X-Served-By: tobaru
  request_id_header_name: string   # Add a unique request ID header to the request
  response_id_header_name: string  # Add a unique request ID header to the response

The address, addresses, and location field names are accepted as aliases for locations.

Backend reuse is local to a client connection and the selected HTTP action. Changing Host/header rules to select another action opens a different backend connection, even when the path prefix is unchanged. Round-robin selection occurs when a backend connection is opened, not on every request.

Header names in patches are case-insensitive. Repeated received fields retain their value order; an overwrite replaces all values for that name. Patches must not change the received body's framing (for example, changing Content-Length or removing chunked Transfer-Encoding). Such requests/responses are rejected and the connection is closed; body bytes are not rewritten to match a patched length or transfer-coding stack. Transfer-Encoding fields with parameters must remain unchanged; their quoted values are not normalized. Patches also cannot manufacture or relabel a successful protocol upgrade: the original and forwarded handshakes must agree, including the case-sensitive protocol version when present.

Forwarding supports HEAD/bodyless responses, informational responses including 100 Continue and 103 Early Hints, and close-delimited response bodies. An early final response during an unfinished upload is forwarded and both connections are closed rather than reused on the existing H1-to-H1 path. New H2 paths continue upload alongside successful response DATA for duplex streaming. H1 backend error responses stop upload through write-side shutdown while the response drains. See HTTP/2 Policy and Limits for protocol selection, cancellation, and unsupported features. CONNECT remains unsupported.

serve-message

Returns a static HTTP response.

http_action:
  type: serve-message
  status_code: 200                 # HTTP status code (required)
  status_message: "OK"             # Custom status text (optional)
  content: "Hello, world!"        # Response body (default: "")
  response_headers:                # Custom response headers
    Content-Type: "text/plain"
  response_id_header_name: string  # Add a unique request ID header

serve-directory

Serves static files from a directory. MIME types are automatically detected.

After route selection, the query is removed and the file path is percent-decoded. Decoded parent-directory segments and NUL bytes are rejected; names such as report..final.txt are allowed. Both the requested file and a directory's index.html must resolve inside the canonical serving root. Keep serving trees immutable or trusted: canonicalization does not eliminate concurrent symlink replacement races.

http_action:
  type: serve-directory
  path: /var/www/static            # Directory path (required)
  response_headers:                # Custom response headers
    Cache-Control: "max-age=3600"
  response_id_header_name: string  # Add a unique request ID header

close

Immediately closes the connection.

http_action:
  type: close

# Shorthand string form
http_action: close

UDP Targets

UDP forwarding with round-robin load balancing and stateful association tracking.

The optional listener-level udp_max_associations caps the total across all targets. Omitted or null means unlimited; configured values must be positive integers. At capacity, packets for new client addresses are dropped, while existing associations continue forwarding. Idle cleanup frees capacity.

- address: 0.0.0.0:53
  transport: udp
  udp_max_associations: 4096  # Optional, shared by all targets on this listener
  target:
    addresses: [string]            # Backend address(es) -- round-robin
    allowlist: string | [string]   # IP masks or group names
    association_timeout_secs: 200  # Timeout for UDP associations (default: 200, min: 5)

The address, location, and locations field names are accepted as aliases for addresses.

IP Groups

Named, reusable sets of IP ranges. Define groups before referencing them in server configs (ordering within the file matters).

# Define groups
- group: internal
  ip_masks:
    - 192.168.0.0/16
    - 10.0.0.0/8
    - 172.16.0.0/12

- group: trusted
  ip_masks:
    - 1.2.3.4
    - 5.6.7.8
    - internal                     # Groups can reference other groups

# Use in server config
- address: 0.0.0.0:8080
  transport: tcp
  target:
    location: backend:8080
    allowlist:
      - internal
      - trusted

The built-in group all is equivalent to ::/0, covering both IPv4 and IPv6. Use 0.0.0.0/0 to allow only IPv4.

ip_mask (singular) is accepted as an alias for ip_masks.

URL-Based Configuration

For simple forwarding, configs can be specified as URLs on the command line instead of YAML files.

# TCP forwarding
tobaru tcp://127.0.0.1:8080?target=192.168.1.10:80

# Multiple targets (round-robin)
tobaru "tcp://127.0.0.1:8080?target=backend1:80&target=backend2:80"

# Forward to UNIX socket
tobaru tcp://127.0.0.1:8080?target-path=/run/app.sock

# UDP forwarding
tobaru udp://127.0.0.1:53?target=8.8.8.8:53&target=8.8.4.4:53

URL configs always use allowlist: 0.0.0.0/0 (allow all). For access control, use a YAML config file.

URL Query Parameters

Scheme Parameter Description
tcp target or target-address Backend TCP address
tcp target-path Backend UNIX socket path
udp target or target-address Backend UDP address

Command Line

tobaru [OPTIONS] <CONFIG PATH or CONFIG URL> [CONFIG PATH or CONFIG URL] ...

OPTIONS:
  -t, --threads NUM           Worker threads (default: auto-detected from CPU count)
  --dry-run                  Validate config, routing, and TLS files without binding listeners
  --clear-iptables-all        Clear all tobaru iptables rules and exit
  --clear-iptables-matching   Clear iptables rules for specified config files and exit
  -h, --help                  Show help

Configuration file changes, including atomic replacements, are debounced for three seconds while current listeners continue serving. Invalid replacement configuration retains the last-good listeners. Valid replacements stop and await old listener tasks before rebinding, so reload is not gapless. Existing TCP sessions keep their old configuration until completion; UDP associations restart. Unexpected listener errors or panics exit the process nonzero. Bind and firewall failures also exit nonzero, including during reload. --dry-run does not verify that ports can be bound or firewall commands can succeed.

Examples

# Run with a single config file
tobaru config.yaml

# Run with multiple config files (merged together)
tobaru servers.yaml ip_groups.yaml

# Simple TCP forwarding via URL
tobaru tcp://127.0.0.1:8080?target=192.168.1.10:80

# Custom thread count
tobaru --threads 4 config.yaml

# Clear iptables rules
sudo tobaru --clear-iptables-matching config.yaml
sudo tobaru --clear-iptables-all

Hot Reloading

Configuration files are automatically watched for changes and reloaded without restart.

Deprecated Fields

These fields are accepted with a deprecation warning and will be removed in a future version.

Deprecated Replacement
serverTls server_tls
bindAddress address
addresses (in targets) locations
optional (in server_tls) true auto-migrates to TLS and non-TLS targets; raw forwarding also accepts unmatched/malformed TLS. See migration notes.