From f5ff1b3ab84a3609166dc2a5a329854efd24e7b3 Mon Sep 17 00:00:00 2001 From: Martin Dobrev Date: Wed, 14 Jan 2026 14:12:08 +0000 Subject: [PATCH] Add comprehensive documentation, examples, and new DNSBL providers Documentation (docs/): - Architecture overview with flow diagrams - Complete API reference for all functions - Configuration guide with step-by-step setup - DNSBL conceptual explanation - HTTP headers reference - Troubleshooting guide Examples (examples/): - Basic configuration - Behind-proxy setup (X-Forwarded-For, Cloudflare) - Multiple DNSBL providers - Docker test environment - Logging configurations New Features (v0.4.0): - Add support for .exitlist.torproject.org domain - Add support for Spamhaus domains (sbl, xbl, pbl, zen) - Add configurable track-sc index (sc0, sc1, sc2) - Add X-DNSBL-Zone and X-DNSBL-Description headers - Add explicit error messages for unsupported stick-table types --- README.md | 173 +++++++-- docs/README.md | 50 +++ docs/api-reference.md | 351 ++++++++++++++++++ docs/architecture.md | 237 ++++++++++++ docs/configuration.md | 351 ++++++++++++++++++ docs/dnsbl-explained.md | 334 +++++++++++++++++ docs/http-headers.md | 307 ++++++++++++++++ docs/troubleshooting.md | 457 ++++++++++++++++++++++++ examples/basic/README.md | 63 ++++ examples/basic/haproxy.cfg | 59 +++ examples/behind-proxy/README.md | 65 ++++ examples/behind-proxy/haproxy.cfg | 96 +++++ examples/docker/README.md | 118 ++++++ examples/docker/backend/html/index.html | 64 ++++ examples/docker/docker-compose.yml | 35 ++ examples/docker/haproxy/Dockerfile | 38 ++ examples/docker/haproxy/dnsbl.lua | 346 ++++++++++++++++++ examples/docker/haproxy/haproxy.cfg | 60 ++++ examples/logging/README.md | 78 ++++ examples/logging/haproxy.cfg | 94 +++++ examples/multi-dnsbl/README.md | 124 +++++++ examples/multi-dnsbl/haproxy.cfg | 119 ++++++ src/dnsbl.lua | 111 +++++- 23 files changed, 3694 insertions(+), 36 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/api-reference.md create mode 100644 docs/architecture.md create mode 100644 docs/configuration.md create mode 100644 docs/dnsbl-explained.md create mode 100644 docs/http-headers.md create mode 100644 docs/troubleshooting.md create mode 100644 examples/basic/README.md create mode 100644 examples/basic/haproxy.cfg create mode 100644 examples/behind-proxy/README.md create mode 100644 examples/behind-proxy/haproxy.cfg create mode 100644 examples/docker/README.md create mode 100644 examples/docker/backend/html/index.html create mode 100644 examples/docker/docker-compose.yml create mode 100644 examples/docker/haproxy/Dockerfile create mode 100644 examples/docker/haproxy/dnsbl.lua create mode 100644 examples/docker/haproxy/haproxy.cfg create mode 100644 examples/logging/README.md create mode 100644 examples/logging/haproxy.cfg create mode 100644 examples/multi-dnsbl/README.md create mode 100644 examples/multi-dnsbl/haproxy.cfg diff --git a/README.md b/README.md index 88bcf75..03ab9fd 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,167 @@ # haproxy-lua-dnsbl -DNSBL (DNS blacklisting) module for HAProxy Lua. Dynamically block requests based on the response of the DNS query or use -any of the added extra request headers to make decissions down the line. +DNSBL (DNS blacklisting) module for HAProxy Lua. Dynamically block requests based on the response of the DNS query or use any of the added extra request headers to make decisions down the line. -# Installation +## Features -`haproxy-lua-dnsbl` is depending on [utils](https://github.com/dobrevit/haproxy-lua-utils) and `socket` library. Download -a copy of them in Lua accessible path, for example `/usr/share/lua/5.3/`. +- Query multiple DNSBL providers (Tor exit lists, Spamhaus, etc.) +- Cache results using HAProxy stick-tables for optimal performance +- Configurable track-sc index (sc0, sc1, sc2) for flexible stick-table usage +- Detailed HTTP headers for logging and downstream processing +- Fail-open design - errors don't block legitimate traffic -# Usage +## Supported DNSBL Providers -Please have a look at the [example](./example/) folder for inspiration how to use this library. +| Provider | Domain | Description | +|----------|--------|-------------| +| Dan.me.uk Tor List | `.torexit.dan.me.uk` | Tor exit node list | +| Tor Project | `.exitlist.torproject.org` | Official Tor exit list | +| Spamhaus SBL | `sbl.spamhaus.org` | Spam sources | +| Spamhaus XBL | `xbl.spamhaus.org` | Exploits/botnets | +| Spamhaus PBL | `pbl.spamhaus.org` | Policy block list | +| Spamhaus ZEN | `zen.spamhaus.org` | Combined list | -# How it works +## Quick Start -The library is using the `socket.dns` class to make DNS queries. The DNSBL domain is configured in the `dnsbl_domain` -variable. The DNSBL domain is expected to return a `NXDOMAIN` response if the IP address is not blacklisted. If the -response is `NXDOMAIN`, the request is allowed to pass through. If there is a response, the request is marked for denying. -Finally, based on the results, the client IP will flip a set of gpc counters that will act as a cache for the next -requests. -You can read more about the design of the library in the [docs](./docs/) section. +```haproxy +global + lua-load /usr/share/lua/5.3/dnsbl.lua -# Known limitations +backend st_dnsbl_cache + stick-table type ipv6 size 1m expire 30m store gpc0,gpc1 -I'm unable to find a reliable way to call `do-resolve` from within Lua, so I decided to fall back to the Lua `socket.dns` -class and use it to make DNS queries. For this reason only A records are supported. -Next to it, although the code is making provisions for configuring the DNSBL domain, only `.torexit.dan.me.uk` is supported. +frontend http-in + bind *:80 + http-request track-sc0 src table st_dnsbl_cache + http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk "" "" + http-request lua.dnsbl_block st_dnsbl_cache + default_backend servers +``` -# TODO +## Installation -* [X] Add support for `.torexit.dan.me.uk` domain -* [ ] Add support for `.exitlist.torproject.org` domain -* [ ] Add support for `xbl.spamhaus.org` domain -* [ ] Make it possible to configure the cache and ban track-sc index (hard-coded for now) +`haproxy-lua-dnsbl` depends on [utils](https://github.com/dobrevit/haproxy-lua-utils) and `socket` library. Download copies to your Lua path: -# Contributing +```bash +# Install to Lua path (e.g., /usr/share/lua/5.3/) +cp src/dnsbl.lua /usr/share/lua/5.3/ + +# Install dependencies +wget -O /usr/share/lua/5.3/utils.lua \ + https://raw.githubusercontent.com/dobrevit/haproxy-lua-utils/main/src/utils.lua +``` + +## Documentation + +Full documentation is available in the [docs](./docs/) folder: + +- [Documentation Index](./docs/README.md) +- [Architecture Overview](./docs/architecture.md) - How the module works +- [API Reference](./docs/api-reference.md) - Complete function documentation +- [Configuration Guide](./docs/configuration.md) - HAProxy setup instructions +- [Understanding DNSBL](./docs/dnsbl-explained.md) - How DNS blacklists work +- [HTTP Headers Reference](./docs/http-headers.md) - Headers set by the module +- [Troubleshooting](./docs/troubleshooting.md) - Common issues and solutions + +## Examples + +The [examples](./examples/) folder contains ready-to-use configurations: + +| Example | Description | +|---------|-------------| +| [Basic](./examples/basic/) | Minimal working configuration | +| [Behind Proxy](./examples/behind-proxy/) | Using X-Forwarded-For header | +| [Multiple DNSBLs](./examples/multi-dnsbl/) | Querying multiple providers | +| [Docker](./examples/docker/) | Complete Docker test environment | +| [Logging](./examples/logging/) | Custom log formats with DNSBL headers | + +## Usage + +### Basic Usage + +```haproxy +http-request lua.dnsbl_query [sc_index] +http-request lua.dnsbl_block +``` + +### Parameters + +| Parameter | Description | +|-----------|-------------| +| `backend` | Backend name containing the stick-table | +| `domain` | DNSBL domain (e.g., `.torexit.dan.me.uk`) | +| `src_var` | HAProxy variable with client IP (optional) | +| `src_header` | HTTP header with client IP (optional) | +| `sc_index` | Track-sc index: 0, 1, or 2 (optional, default: 0) | + +### Examples + +```haproxy +# Basic - direct client IP +http-request lua.dnsbl_query st_cache .torexit.dan.me.uk "" "" + +# Behind proxy - use X-Forwarded-For +http-request lua.dnsbl_query st_cache .torexit.dan.me.uk "" X-Forwarded-For + +# Use sc1 instead of sc0 +http-request lua.dnsbl_query st_cache .torexit.dan.me.uk "" "" 1 + +# Multiple providers with different track-sc indices +http-request track-sc0 src table st_tor +http-request track-sc1 src table st_spam +http-request lua.dnsbl_query st_tor .torexit.dan.me.uk "" "" 0 +http-request lua.dnsbl_query st_spam xbl.spamhaus.org "" "" 1 +``` + +## How it Works + +The library uses the `socket.dns` class to make DNS queries. The DNSBL domain is expected to return: + +- **NXDOMAIN** - IP is not blacklisted (request allowed) +- **A record response** - IP is blacklisted (request blocked) + +Results are cached using HAProxy's stick-table with `gpc0` and `gpc1` counters: +- `gpc0 = 1` → IP was checked and allowed +- `gpc1 = 1` → IP was checked and blocked + +## HTTP Headers + +The module sets these headers on each request: + +| Header | Description | +|--------|-------------| +| `X-DNSBL-Action` | Result: CACHE-ALLOW, LOOKUP-DENY, etc. | +| `X-DNSBL-Is-Allowed` | 1 if allowed, 0 if blocked | +| `X-DNSBL-Client-IP` | IP address that was checked | +| `X-DNSBL-Query` | DNS query that was made | +| `X-DNSBL-Zone` | Spamhaus zone (if applicable) | +| `X-DNSBL-Description` | Block reason (if applicable) | + +## Changelog + +### v0.4.0 (Current) + +- Added support for `.exitlist.torproject.org` domain +- Added support for Spamhaus domains (sbl, xbl, pbl, zen) +- Added configurable track-sc index (sc0, sc1, sc2) +- Added `X-DNSBL-Zone` and `X-DNSBL-Description` headers +- Added comprehensive documentation and examples + +### v0.3.0 + +- Initial public release +- Support for `.torexit.dan.me.uk` domain + +## Known Limitations + +- Only A record DNS queries are supported (not AAAA) +- Spamhaus may rate-limit queries from public DNS resolvers + +## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/dobrevit/haproxy-lua-dnsbl/issues. -# License +## License MIT License diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..a4fbfb6 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,50 @@ +# HAProxy Lua DNSBL Documentation + +Welcome to the HAProxy Lua DNSBL documentation. This module enables dynamic request blocking based on DNS blacklist lookups directly within HAProxy. + +## Quick Start + +1. [Installation & Configuration](configuration.md) - Get up and running quickly +2. [Basic Example](../examples/basic/haproxy.cfg) - Minimal working configuration + +## Documentation + +| Document | Description | +|----------|-------------| +| [Architecture Overview](architecture.md) | How the module works, request flow, and caching mechanism | +| [API Reference](api-reference.md) | Complete function documentation with parameters and examples | +| [Configuration Guide](configuration.md) | HAProxy setup, stick-tables, and module loading | +| [Understanding DNSBL](dnsbl-explained.md) | How DNS blacklists work and supported providers | +| [HTTP Headers Reference](http-headers.md) | All headers set by the module | +| [Troubleshooting](troubleshooting.md) | Common issues and debugging tips | + +## Examples + +| Example | Description | +|---------|-------------| +| [Basic Setup](../examples/basic/) | Minimal configuration using direct client IP | +| [Behind Proxy](../examples/behind-proxy/) | Using X-Forwarded-For when behind a reverse proxy | +| [Multiple DNSBLs](../examples/multi-dnsbl/) | Querying multiple blacklist providers | +| [Docker Environment](../examples/docker/) | Complete Docker-based test environment | +| [Logging](../examples/logging/) | Custom log formats with DNSBL headers | + +## Supported DNSBL Providers + +| Provider | Domain | Response Code | Description | +|----------|--------|---------------|-------------| +| Dan.me.uk Tor Exit | `.torexit.dan.me.uk` | `127.0.0.100` | Tor exit node list | +| Tor Project Exit List | `.exitlist.torproject.org` | `127.0.0.2` | Official Tor exit list | +| Spamhaus XBL | `xbl.spamhaus.org` | Various | Exploits Block List | +| Spamhaus ZEN | `zen.spamhaus.org` | Various | Combined Spamhaus list | + +## Version + +Current version: **0.4.0** + +## License + +MIT License - See [LICENSE](../LICENSE) for details. + +## Contributing + +Bug reports and pull requests are welcome on GitHub at https://github.com/dobrevit/haproxy-lua-dnsbl/issues. diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..3a8e483 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,351 @@ +# API Reference + +Complete documentation for all functions in the HAProxy Lua DNSBL module. + +## HAProxy Actions + +These actions are registered with HAProxy and can be used in your configuration. + +--- + +### lua.dnsbl_query + +Performs a DNSBL lookup for the client IP address and caches the result. + +**Usage in HAProxy:** +```haproxy +http-request lua.dnsbl_query [src_var] [src_header] [sc_index] +``` + +**Parameters:** + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `stick_table_backend` | Yes | Name of the backend containing the stick-table for caching | +| `dnsbl_domain` | Yes | DNSBL domain to query (e.g., `.torexit.dan.me.uk`) | +| `src_var` | No | HAProxy variable containing the client IP (e.g., `txn.real_ip`). Use `""` to skip. | +| `src_header` | No | HTTP header containing the client IP (e.g., `X-Forwarded-For`). Use `""` to skip. | +| `sc_index` | No | Track-sc index (0, 1, or 2) for incrementing gpc counters. Must match the `track-sc` index used. Defaults to 0. | + +**Client IP Resolution Order:** +1. Transaction variable `txn.dnsbl_client_ip` (if previously set) +2. Custom variable specified in `src_var` +3. HTTP header specified in `src_header` +4. Direct connection source (`txn.sf:src()`) + +**Examples:** + +```haproxy +# Basic usage - use direct client IP (uses track-sc0 by default) +http-request lua.dnsbl_query st_cache .torexit.dan.me.uk "" "" + +# Behind a proxy - get IP from X-Forwarded-For header +http-request lua.dnsbl_query st_cache .torexit.dan.me.uk "" X-Forwarded-For + +# Using a custom variable +http-request set-var(txn.client_ip) hdr(X-Real-IP) +http-request lua.dnsbl_query st_cache .torexit.dan.me.uk txn.client_ip "" + +# Using track-sc1 instead of track-sc0 +http-request track-sc1 src table st_cache +http-request lua.dnsbl_query st_cache .torexit.dan.me.uk "" "" 1 + +# Multiple DNSBL providers with separate stick-tables +# IMPORTANT: sc_index must match the track-sc index used for each table +http-request track-sc0 src table st_tor_cache +http-request track-sc1 src table st_spam_cache +http-request lua.dnsbl_query st_tor_cache .torexit.dan.me.uk "" "" 0 +http-request lua.dnsbl_query st_spam_cache xbl.spamhaus.org "" "" 1 +``` + +**Headers Set:** + +| Header | Description | +|--------|-------------| +| `X-DNSBL-Action` | Result of the lookup (see below) | +| `X-DNSBL-Is-Allowed` | `1` if allowed, `0` if blocked | +| `X-DNSBL-Version` | Module version | +| `X-DNSBL-Client-IP` | IP address that was checked | +| `X-DNSBL-Query` | Full DNS query string | +| `X-DNSBL-Error` | Error message (if applicable) | +| `X-DNSBL-Zone` | Spamhaus zone (if applicable) | +| `X-DNSBL-Description` | Spamhaus description (if applicable) | + +**X-DNSBL-Action Values:** + +| Value | Meaning | +|-------|---------| +| `DNSBL-CACHE-ALLOW` | Cached result: IP is allowed | +| `DNSBL-CACHE-DENY` | Cached result: IP is blocked | +| `DNSBL-LOOKUP-ALLOW` | Fresh lookup: IP not in blacklist | +| `DNSBL-LOOKUP-DENY` | Fresh lookup: IP found in blacklist | +| `DNSBL-ERROR-ALLOW` | Error occurred, request allowed (fail-open) | + +**Transaction Variables Set:** + +| Variable | Description | +|----------|-------------| +| `txn.dnsbl_client_ip` | Client IP used for the lookup | +| `txn.dnsbl_is_allowed` | Boolean indicating if request is allowed | + +--- + +### lua.dnsbl_block + +Blocks requests from IPs that were marked as blocked by `dnsbl_query`. + +**Usage in HAProxy:** +```haproxy +http-request lua.dnsbl_block +``` + +**Parameters:** + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `stick_table_backend` | Yes | Name of the backend containing the stick-table | + +**Behavior:** +- Checks `txn.dnsbl_is_allowed` variable set by `dnsbl_query` +- If not allowed, returns `401 Unauthorized` response +- Response includes `Denial-Reason` header + +**Example:** +```haproxy +frontend http-in + bind *:80 + + # First, perform the lookup + http-request lua.dnsbl_query st_cache .torexit.dan.me.uk "" "" + + # Then, block if necessary + http-request lua.dnsbl_block st_cache + + default_backend servers +``` + +**Response on Block:** +```http +HTTP/1.1 401 Unauthorized +Content-Type: text/html +Server: DNSBL/0.4.0 +Denial-Reason: DNSBL: IP found in hard banlist. BLOCK request +``` + +--- + +## Lua Module Functions + +These functions are exported by the module and can be used in custom Lua code. + +### _M.stktbl_lookup + +Looks up an IP address in a HAProxy stick-table. + +**Signature:** +```lua +local entry, err = _M.stktbl_lookup(stktbl, key) +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `stktbl` | stick-table | HAProxy stick-table object | +| `key` | string | IP address to look up | + +**Returns:** + +| Return | Type | Description | +|--------|------|-------------| +| `entry` | table/nil | Stick-table entry with gpc0, gpc1, etc. | +| `err` | string/nil | Error message if lookup failed | + +**Behavior:** +- For IPv6 stick-tables, automatically converts IPv4 addresses to IPv4-mapped IPv6 format (`::ffff:x.x.x.x`) +- Returns `nil, "Unsupported stick-table type"` for unsupported types +- Returns `nil, "No entry found"` if IP not in table + +**Example:** +```lua +local st = core.backends["st_cache"].stktable +local entry, err = _M.stktbl_lookup(st, "192.0.2.1") + +if entry then + if entry.gpc0 == 1 then + -- IP is allowed + elseif entry.gpc1 == 1 then + -- IP is blocked + end +else + -- New visitor or error + print("Lookup error: " .. (err or "unknown")) +end +``` + +--- + +### _M.spamhaus_response + +Maps Spamhaus DNSBL response codes to human-readable information. + +**Signature:** +```lua +local permitted, zone, description = _M.spamhaus_response(response) +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `response` | string | IP address returned by Spamhaus DNS query | + +**Returns:** + +| Return | Type | Description | +|--------|------|-------------| +| `permitted` | boolean | `true` if request should be allowed | +| `zone` | string/nil | Spamhaus zone (SBL, XBL, PBL) | +| `description` | string/nil | Human-readable description | + +**Response Code Mappings:** + +| Response | Zone | Description | Permitted | +|----------|------|-------------|-----------| +| `127.0.0.2` | SBL | Spamhaus SBL Data | false | +| `127.0.0.3` | SBL | Spamhaus SBL CSS Data | false | +| `127.0.0.4` | XBL | CBL Data | false | +| `127.0.0.9` | SBL | Spamhaus DROP/EDROP Data | false | +| `127.0.0.10` | PBL | ISP Maintained | false | +| `127.0.0.11` | PBL | Spamhaus Maintained | false | +| `127.255.255.252` | Any | Typing error in DNSBL name | false | +| `127.255.255.254` | Any | Query via public/open resolver | false | +| `127.255.255.255` | Any | Excessive number of queries | false | +| Other | - | Unknown response | true | + +**Example:** +```lua +local ip = "127.0.0.4" -- Response from Spamhaus +local permitted, zone, description = _M.spamhaus_response(ip) + +if not permitted then + print(string.format("Blocked by %s: %s", zone, description)) + -- Output: Blocked by XBL: CBL Data +end +``` + +--- + +### _M.is_blocked_response + +Checks if a DNSBL response indicates the IP should be blocked. + +**Signature:** +```lua +local blocked, zone, description = _M.is_blocked_response(response, dnsbl_domain) +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `response` | string | IP address returned by DNSBL DNS query | +| `dnsbl_domain` | string | The DNSBL domain that was queried | + +**Returns:** + +| Return | Type | Description | +|--------|------|-------------| +| `blocked` | boolean | `true` if IP should be blocked | +| `zone` | string/nil | Zone/list name (for Spamhaus) | +| `description` | string/nil | Description (for Spamhaus) | + +**Supported DNSBL Domains:** + +| Domain | Blocked Responses | +|--------|-------------------| +| `.torexit.dan.me.uk` | `127.0.0.100` | +| `.exitlist.torproject.org` | `127.0.0.2` | +| `*.spamhaus.org` | Uses `spamhaus_response()` mapping | + +**Example:** +```lua +local response = "127.0.0.100" +local domain = ".torexit.dan.me.uk" + +local blocked, zone, desc = _M.is_blocked_response(response, domain) +if blocked then + print("IP is blocked: Tor exit node detected") +end +``` + +--- + +### _M.sc_inc_gpc0 + +Increments gpc0 counter for the specified stick-table using configurable track-sc index. + +**Signature:** +```lua +_M.sc_inc_gpc0(txn, backend, sc_index) +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `txn` | transaction | HAProxy transaction object | +| `backend` | string | Backend name with stick-table | +| `sc_index` | number | Track-sc index (0, 1, or 2) | + +**Example:** +```lua +-- Increment gpc0 using sc1 +_M.sc_inc_gpc0(txn, "st_cache", 1) +``` + +--- + +### _M.sc_inc_gpc1 + +Increments gpc1 counter for the specified stick-table using configurable track-sc index. + +**Signature:** +```lua +_M.sc_inc_gpc1(txn, backend, sc_index) +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `txn` | transaction | HAProxy transaction object | +| `backend` | string | Backend name with stick-table | +| `sc_index` | number | Track-sc index (0, 1, or 2) | + +--- + +## Module Properties + +### _M.version + +Current module version string. + +**Type:** string + +**Example:** +```lua +print(_M.version) -- "0.4.0" +``` + +--- + +## Dependencies + +The module requires the following Lua libraries: + +| Library | Purpose | +|---------|---------| +| `utils` | IP address utilities ([haproxy-lua-utils](https://github.com/dobrevit/haproxy-lua-utils)) | +| `socket` | DNS resolution | +| `inspect` | Debug output formatting | diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..2d48395 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,237 @@ +# Architecture Overview + +This document explains how the HAProxy Lua DNSBL module works, including the request flow, caching mechanism, and component interactions. + +## High-Level Architecture + +``` + ┌─────────────────────────────────────────┐ + │ HAProxy │ + │ │ + ┌──────────┐ │ ┌────────────────────────────────────┐ │ + │ Client │ ─── HTTP Request ──►│ │ Frontend │ │ + │ │ │ │ │ │ + └──────────┘ │ │ http-request lua.dnsbl_query │ │ + ▲ │ │ http-request lua.dnsbl_block │ │ + │ │ └─────────────┬──────────────────────┘ │ + │ │ │ │ + │ │ ▼ │ + │ │ ┌────────────────────────────────────┐ │ + │ │ │ dnsbl.lua │ │ + │ │ │ │ │ + │ │ │ 1. Get client IP │ │ + │ │ │ 2. Check stick-table cache │ │ + │ │ │ 3. DNS lookup (if needed) │ │ + │ │ │ 4. Update cache │ │ + │ │ │ 5. Set response headers │ │ + │ │ │ 6. Block or allow │ │ + │ │ └──────┬───────────────┬─────────────┘ │ + │ │ │ │ │ + │ │ ▼ ▼ │ + │ │ ┌────────────┐ ┌─────────────────┐ │ + │ │ │ Stick-Table│ │ DNS Resolver │ │ + │ │ │ Cache │ │ │ │ + │ │ │ │ │ DNSBL Query: │ │ + │ │ │ gpc0=allow │ │ 1.2.0.192. │ │ + │ │ │ gpc1=deny │ │ torexit.dan. │ │ + │ │ │ │ │ me.uk │ │ + │ │ └────────────┘ └────────┬────────┘ │ + │ │ │ │ + │ └───────────────────────────┼─────────────┘ + │ │ + │ 401 Unauthorized ▼ + │ (if blocked) ┌─────────────┐ + └────────────────────────────────────────────────│ DNSBL │ + │ Server │ + └─────────────┘ +``` + +## Components + +### 1. dnsbl.lua Module + +The core Lua module that provides two HAProxy actions: + +| Action | Purpose | +|--------|---------| +| `lua.dnsbl_query` | Performs the DNSBL lookup and caches the result | +| `lua.dnsbl_block` | Blocks the request if the IP is blacklisted | + +### 2. Stick-Table Cache + +HAProxy stick-tables are used to cache DNSBL lookup results, avoiding repeated DNS queries for the same IP address. + +``` +┌────────────────────────────────────────────────────────────┐ +│ Stick-Table Entry │ +├──────────────┬──────────────┬──────────────┬───────────────┤ +│ Key │ gpc0 │ gpc1 │ Expiry │ +│ (IP addr) │ (allowed) │ (blocked) │ (TTL) │ +├──────────────┼──────────────┼──────────────┼───────────────┤ +│ 192.0.2.1 │ 1 │ 0 │ 30 minutes │ +│ 198.51.100.1 │ 0 │ 1 │ 30 minutes │ +└──────────────┴──────────────┴──────────────┴───────────────┘ +``` + +**Counter meanings:** +- `gpc0 = 1` → IP was checked and **allowed** (not in blacklist) +- `gpc1 = 1` → IP was checked and **blocked** (found in blacklist) +- Both `0` → IP not yet checked (new visitor) + +### 3. DNS Resolution + +The module uses Lua's `socket.dns` library to perform DNS queries. + +**Query format:** +``` +{reversed_ip}.{dnsbl_domain} + +Example: + Client IP: 192.0.2.100 + Reversed: 100.2.0.192 + Query: 100.2.0.192.torexit.dan.me.uk +``` + +## Request Flow + +### Flow 1: New Visitor (Cache Miss) + +``` +┌──────┐ ┌─────────┐ ┌───────────┐ ┌──────────┐ ┌────────┐ +│Client│ │ HAProxy │ │ dnsbl.lua │ │Stick-Tbl │ │ DNSBL │ +└──┬───┘ └────┬────┘ └─────┬─────┘ └────┬─────┘ └───┬────┘ + │ │ │ │ │ + │ HTTP Request │ │ │ │ + │─────────────►│ │ │ │ + │ │ dnsbl_query() │ │ │ + │ │───────────────►│ │ │ + │ │ │ lookup(IP) │ │ + │ │ │───────────────►│ │ + │ │ │ gpc0=0,gpc1=0 │ │ + │ │ │◄───────────────│ │ + │ │ │ │ │ + │ │ │ DNS query │ │ + │ │ │───────────────────────────────►│ + │ │ │ │ NXDOMAIN │ + │ │ │◄───────────────────────────────│ + │ │ │ │ │ + │ │ │ inc_gpc0() │ │ + │ │ │───────────────►│ │ + │ │ │ │ │ + │ │ Set headers │ │ │ + │ │◄───────────────│ │ │ + │ │ │ │ │ + │ 200 OK │ │ │ │ + │◄─────────────│ │ │ │ +``` + +### Flow 2: Returning Visitor (Cache Hit - Allowed) + +``` +┌──────┐ ┌─────────┐ ┌───────────┐ ┌──────────┐ +│Client│ │ HAProxy │ │ dnsbl.lua │ │Stick-Tbl │ +└──┬───┘ └────┬────┘ └─────┬─────┘ └────┬─────┘ + │ │ │ │ + │ HTTP Request │ │ │ + │─────────────►│ │ │ + │ │ dnsbl_query() │ │ + │ │───────────────►│ │ + │ │ │ lookup(IP) │ + │ │ │───────────────►│ + │ │ │ gpc0=1 │ ◄── Cache hit! + │ │ │◄───────────────│ + │ │ │ │ + │ │ Headers: CACHE-ALLOW │ + │ │◄───────────────│ │ + │ │ │ │ + │ 200 OK │ │ (No DNS query needed) + │◄─────────────│ │ +``` + +### Flow 3: Blocked Visitor + +``` +┌──────┐ ┌─────────┐ ┌───────────┐ ┌──────────┐ +│Client│ │ HAProxy │ │ dnsbl.lua │ │Stick-Tbl │ +└──┬───┘ └────┬────┘ └─────┬─────┘ └────┬─────┘ + │ │ │ │ + │ HTTP Request │ │ │ + │─────────────►│ │ │ + │ │ dnsbl_query() │ │ + │ │───────────────►│ │ + │ │ │ lookup(IP) │ + │ │ │───────────────►│ + │ │ │ gpc1=1 │ ◄── Blacklisted! + │ │ │◄───────────────│ + │ │ Headers: CACHE-DENY │ + │ │◄───────────────│ │ + │ │ │ │ + │ │ dnsbl_block() │ │ + │ │───────────────►│ │ + │ │ 401 reply │ │ + │ │◄───────────────│ │ + │ │ │ │ + │ 401 Unauth. │ │ │ + │◄─────────────│ │ +``` + +## Caching Strategy + +### Why Cache? + +1. **Performance** - DNS queries add latency (typically 10-100ms) +2. **Rate limiting** - Some DNSBLs limit query frequency +3. **Reliability** - Cached results available even if DNSBL is down + +### Cache Parameters + +| Parameter | Configuration | Purpose | +|-----------|--------------|---------| +| TTL | `expire` in stick-table | How long to cache results | +| Size | `size` in stick-table | Maximum number of cached IPs | +| Type | `type ipv6` | Stick-table key type | + +### Recommended Settings + +```haproxy +backend st_dnsbl_cache + stick-table type ipv6 size 1m expire 30m store gpc0,gpc1 +``` + +- **size 1m** - Store up to 1 million IP addresses +- **expire 30m** - Cache results for 30 minutes +- **type ipv6** - Supports both IPv4 and IPv6 (IPv4 mapped to ::ffff:x.x.x.x) + +## IPv6 Support + +The module handles IPv4-mapped IPv6 addresses automatically: + +``` +IPv4 address: 192.0.2.1 +IPv6 mapped: ::ffff:192.0.2.1 +``` + +This ensures consistent stick-table lookups regardless of whether HAProxy sees the original IPv4 or the mapped IPv6 format. + +## Error Handling + +| Scenario | Behavior | +|----------|----------| +| DNS timeout | Request allowed, header set to `DNSBL-ERROR-ALLOW` | +| Stick-table missing | Debug message logged, request allowed | +| Invalid IP format | Debug message logged, request blocked | +| DNSBL unreachable | Request allowed (fail-open) | + +The module follows a **fail-open** policy - if there's an error checking the blacklist, the request is allowed through. This prevents a DNSBL outage from blocking all traffic. + +## Performance Considerations + +1. **First request per IP** - Adds DNS query latency (~10-100ms) +2. **Cached requests** - Minimal overhead (stick-table lookup) +3. **Memory usage** - ~200 bytes per cached IP address + +### Tuning Tips + +- Increase stick-table `expire` time for lower DNS query volume +- Increase stick-table `size` for high-traffic sites with many unique visitors +- Use local DNS resolver for faster DNSBL queries diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..6ca5a18 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,351 @@ +# Configuration Guide + +This guide covers everything you need to configure HAProxy Lua DNSBL module. + +## Prerequisites + +### 1. HAProxy with Lua Support + +Ensure HAProxy is compiled with Lua support: + +```bash +haproxy -vv | grep Lua +# Should show: Built with Lua version +``` + +### 2. Required Lua Libraries + +Install the required dependencies in your Lua path (e.g., `/usr/share/lua/5.3/`): + +| Library | Repository | Purpose | +|---------|------------|---------| +| `utils.lua` | [haproxy-lua-utils](https://github.com/dobrevit/haproxy-lua-utils) | IP address utilities | +| `socket` | LuaSocket | DNS resolution (usually pre-installed) | +| `inspect.lua` | [inspect.lua](https://github.com/kikito/inspect.lua) | Debug output (optional) | + +```bash +# Example installation +cd /usr/share/lua/5.3/ +wget https://raw.githubusercontent.com/dobrevit/haproxy-lua-utils/main/src/utils.lua +wget https://raw.githubusercontent.com/kikito/inspect.lua/master/inspect.lua +``` + +### 3. Install DNSBL Module + +```bash +cp src/dnsbl.lua /usr/share/lua/5.3/dnsbl.lua +``` + +--- + +## Basic Configuration + +### Step 1: Load the Lua Module + +Add to your HAProxy configuration's `global` section: + +```haproxy +global + lua-load /usr/share/lua/5.3/dnsbl.lua + # Or if in Lua path: + # lua-load dnsbl.lua +``` + +### Step 2: Create a Stick-Table Backend + +The stick-table caches DNSBL lookup results: + +```haproxy +backend st_dnsbl_cache + # IPv6 type supports both IPv4 and IPv6 addresses + stick-table type ipv6 size 1m expire 30m store gpc0,gpc1 +``` + +**Parameters explained:** + +| Parameter | Value | Description | +|-----------|-------|-------------| +| `type` | `ipv6` | Key type - use ipv6 for dual-stack support | +| `size` | `1m` | Maximum entries (1 million) | +| `expire` | `30m` | Cache TTL (30 minutes) | +| `store` | `gpc0,gpc1` | Counters: gpc0=allowed, gpc1=blocked | + +### Step 3: Configure IP Tracking + +Track client IPs in the stick-table: + +```haproxy +frontend http-in + bind *:80 + + # Track source IP in stick-table + http-request track-sc0 src table st_dnsbl_cache +``` + +### Step 4: Add DNSBL Actions + +```haproxy +frontend http-in + bind *:80 + + http-request track-sc0 src table st_dnsbl_cache + + # Perform DNSBL lookup + http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk "" "" + + # Block blacklisted IPs + http-request lua.dnsbl_block st_dnsbl_cache + + default_backend servers +``` + +--- + +## Complete Configuration Example + +```haproxy +global + log stdout format raw local0 + lua-load /usr/share/lua/5.3/dnsbl.lua + +defaults + log global + mode http + option httplog + timeout connect 5s + timeout client 30s + timeout server 30s + +# Stick-table for caching DNSBL results +backend st_dnsbl_cache + stick-table type ipv6 size 1m expire 30m store gpc0,gpc1 + +# Your backend servers +backend servers + server web1 127.0.0.1:8080 check + +# Frontend with DNSBL protection +frontend http-in + bind *:80 + + # Track source IP + http-request track-sc0 src table st_dnsbl_cache + + # DNSBL lookup and blocking + http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk "" "" + http-request lua.dnsbl_block st_dnsbl_cache + + default_backend servers +``` + +--- + +## Configuration Options + +### dnsbl_query Parameters + +```haproxy +http-request lua.dnsbl_query [sc_index] +``` + +| Parameter | Required | Example | Description | +|-----------|----------|---------|-------------| +| `backend` | Yes | `st_dnsbl_cache` | Backend with stick-table | +| `domain` | Yes | `.torexit.dan.me.uk` | DNSBL domain | +| `src_var` | No | `txn.real_ip` | Variable with client IP (use `""` to skip) | +| `src_header` | No | `X-Forwarded-For` | Header with client IP (use `""` to skip) | +| `sc_index` | No | `0`, `1`, or `2` | Track-sc index for gpc counters (must match `track-sc` index, defaults to 0) | + +### Supported DNSBL Domains + +| Domain | Description | +|--------|-------------| +| `.torexit.dan.me.uk` | Dan.me.uk Tor exit list | +| `.exitlist.torproject.org` | Tor Project exit list | +| `xbl.spamhaus.org` | Spamhaus XBL (exploits) | +| `zen.spamhaus.org` | Spamhaus ZEN (combined) | +| `sbl.spamhaus.org` | Spamhaus SBL (spam) | +| `pbl.spamhaus.org` | Spamhaus PBL (policy) | + +--- + +## Behind a Reverse Proxy + +When HAProxy is behind another proxy (nginx, CDN, etc.), use the `src_header` parameter: + +```haproxy +frontend http-in + bind *:80 + + http-request track-sc0 hdr(X-Forwarded-For) table st_dnsbl_cache + + # Get client IP from X-Forwarded-For header + http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk "" X-Forwarded-For + + http-request lua.dnsbl_block st_dnsbl_cache + + default_backend servers +``` + +### Using X-Real-IP Header + +```haproxy +http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk "" X-Real-IP +``` + +### Using a Custom Variable + +```haproxy +# Extract first IP from X-Forwarded-For (handles multiple proxies) +http-request set-var(txn.real_ip) hdr(X-Forwarded-For),word(1,",") + +http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk txn.real_ip "" +``` + +--- + +## Multiple DNSBL Providers + +You can query multiple blacklists using separate stick-tables: + +```haproxy +# Separate stick-tables per provider +backend st_tor_cache + stick-table type ipv6 size 500k expire 1h store gpc0,gpc1 + +backend st_spam_cache + stick-table type ipv6 size 500k expire 30m store gpc0,gpc1 + +frontend http-in + bind *:80 + + # Track IPs using different track-sc indices + http-request track-sc0 src table st_tor_cache + http-request track-sc1 src table st_spam_cache + + # Check Tor exit list (sc_index 0 matches track-sc0) + http-request lua.dnsbl_query st_tor_cache .torexit.dan.me.uk "" "" 0 + + # Check Spamhaus XBL (sc_index 1 matches track-sc1) + http-request lua.dnsbl_query st_spam_cache xbl.spamhaus.org "" "" 1 + + # Block if either list matched + http-request lua.dnsbl_block st_tor_cache + http-request lua.dnsbl_block st_spam_cache + + default_backend servers +``` + +**Important:** The `sc_index` parameter must match the `track-sc` index used for each stick-table. If they don't match, the gpc counters will be incremented on the wrong stick-table entry, causing the cache to malfunction. + +--- + +## Configurable Track-SC Index + +By default, the module uses `sc0`. To use a different index, pass the `sc_index` parameter (5th parameter): + +```haproxy +# Using sc1 instead of sc0 +http-request track-sc1 src table st_dnsbl_cache +http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk "" "" 1 +``` + +The `sc_index` parameter accepts values `0`, `1`, or `2`, corresponding to `track-sc0`, `track-sc1`, and `track-sc2`. + +--- + +## Stick-Table Sizing Guide + +| Traffic Level | Size | Expire | Memory (approx) | +|--------------|------|--------|-----------------| +| Low (< 10k unique IPs/day) | `100k` | `1h` | ~20 MB | +| Medium (10k-100k) | `500k` | `30m` | ~100 MB | +| High (100k-1M) | `1m` | `15m` | ~200 MB | +| Very High (> 1M) | `5m` | `10m` | ~1 GB | + +**Calculation:** Each entry uses approximately 200 bytes. + +--- + +## Logging Configuration + +Add DNSBL headers to your log format: + +```haproxy +defaults + log-format "%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Ta %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r dnsbl_action:%[req.hdr(X-DNSBL-Action)] dnsbl_allowed:%[req.hdr(X-DNSBL-Is-Allowed)]" +``` + +--- + +## Health Checking + +You may want to exclude health check endpoints from DNSBL: + +```haproxy +frontend http-in + bind *:80 + + # Skip DNSBL for health checks + acl is_health_check path /health /ready /live + + http-request track-sc0 src table st_dnsbl_cache if !is_health_check + http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk "" "" if !is_health_check + http-request lua.dnsbl_block st_dnsbl_cache if !is_health_check + + default_backend servers +``` + +--- + +## Performance Tuning + +### DNS Resolver Configuration + +Use a local caching DNS resolver for best performance: + +```bash +# /etc/resolv.conf +nameserver 127.0.0.1 # Local resolver (dnsmasq, unbound, etc.) +nameserver 8.8.8.8 # Fallback +``` + +### Timeouts + +The Lua socket library uses system defaults. For production, consider running a local DNS resolver to minimize latency. + +### Stick-Table Peers + +For multi-node HAProxy setups, sync stick-tables between nodes: + +```haproxy +peers dnsbl_peers + peer haproxy1 192.168.1.10:10000 + peer haproxy2 192.168.1.11:10000 + +backend st_dnsbl_cache + stick-table type ipv6 size 1m expire 30m store gpc0,gpc1 peers dnsbl_peers +``` + +--- + +## Validation + +Test your configuration: + +```bash +# Syntax check +haproxy -c -f /etc/haproxy/haproxy.cfg + +# Test with a known Tor exit IP (check torproject.org for current list) +curl -H "X-Forwarded-For: " http://localhost/ + +# Check headers returned +curl -v http://localhost/ 2>&1 | grep X-DNSBL +``` + +--- + +## Troubleshooting + +See [Troubleshooting Guide](troubleshooting.md) for common issues and solutions. diff --git a/docs/dnsbl-explained.md b/docs/dnsbl-explained.md new file mode 100644 index 0000000..13ba718 --- /dev/null +++ b/docs/dnsbl-explained.md @@ -0,0 +1,334 @@ +# Understanding DNSBL (DNS Blacklists) + +This guide explains how DNS-based blacklists work and how this module uses them to protect your web applications. + +## What is a DNSBL? + +A **DNSBL** (DNS-based Blackhole List) is a method for publishing a list of IP addresses using the Internet's Domain Name System. These lists typically contain: + +- IP addresses of known spam sources +- Tor exit nodes +- Compromised machines (botnets) +- Open proxies +- Known malicious actors + +### How DNSBL Queries Work + +DNSBL uses a clever trick: it encodes IP addresses into DNS hostnames. The process works like this: + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ DNSBL Query Process │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ Client IP: 192.0.2.100 │ +│ │ +│ Step 1: Reverse the IP octets │ +│ 192.0.2.100 → 100.2.0.192 │ +│ │ +│ Step 2: Append the DNSBL domain │ +│ 100.2.0.192 + .torexit.dan.me.uk │ +│ = 100.2.0.192.torexit.dan.me.uk │ +│ │ +│ Step 3: Perform DNS A record lookup │ +│ Query: 100.2.0.192.torexit.dan.me.uk │ +│ │ +│ Step 4: Interpret the response │ +│ - NXDOMAIN (not found) → IP is NOT blacklisted │ +│ - A record response → IP IS blacklisted │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Why Reverse the IP? + +Reversing the IP address allows DNS servers to efficiently index and search for IP ranges. This is the same technique used in reverse DNS (PTR records) and leverages DNS's hierarchical structure. + +--- + +## Interpreting Responses + +### NXDOMAIN Response + +If the DNS query returns `NXDOMAIN` (Non-Existent Domain), it means: + +- The IP address is **NOT** in the blacklist +- The request should be **ALLOWED** + +### IP Address Response + +If the DNS query returns an IP address (typically in the `127.0.0.x` range), it means: + +- The IP address **IS** in the blacklist +- The request should be **BLOCKED** + +Different response codes can indicate different reasons for blocking: + +``` +Response: 127.0.0.100 → Tor exit node (dan.me.uk) +Response: 127.0.0.2 → Tor exit node (torproject.org) +Response: 127.0.0.4 → Known exploit source (Spamhaus XBL) +``` + +--- + +## Supported DNSBL Providers + +### 1. Dan.me.uk Tor Exit List + +**Domain:** `.torexit.dan.me.uk` + +| Response | Meaning | +|----------|---------| +| `127.0.0.100` | IP is a Tor exit node | +| `NXDOMAIN` | IP is not a Tor exit node | + +**Use case:** Block or flag Tor exit nodes. + +**Example query:** +``` +IP: 185.220.101.1 (example Tor exit) +Query: 1.101.220.185.torexit.dan.me.uk +``` + +### 2. Tor Project Exit List + +**Domain:** `.exitlist.torproject.org` + +The official Tor Project maintains this list. Query format includes the destination port: + +| Response | Meaning | +|----------|---------| +| `127.0.0.2` | IP is a Tor exit node | +| `NXDOMAIN` | IP is not a Tor exit node | + +**Note:** The Tor Project list has specific query formatting requirements. + +### 3. Spamhaus Lists + +Spamhaus operates several blacklists, each with specific response codes: + +#### XBL (Exploits Block List) + +**Domain:** `xbl.spamhaus.org` + +Lists IP addresses of hijacked PCs infected with illegal third-party exploits. + +| Response | Zone | Description | +|----------|------|-------------| +| `127.0.0.4` | XBL | CBL Data (Composite Blocking List) | + +#### SBL (Spamhaus Block List) + +**Domain:** `sbl.spamhaus.org` + +Lists IP addresses from which Spamhaus has seen spam being sent. + +| Response | Zone | Description | +|----------|------|-------------| +| `127.0.0.2` | SBL | Spamhaus SBL Data | +| `127.0.0.3` | SBL | Spamhaus SBL CSS Data | +| `127.0.0.9` | SBL | Spamhaus DROP/EDROP Data | + +#### PBL (Policy Block List) + +**Domain:** `pbl.spamhaus.org` + +Lists IP ranges that should not be sending unauthenticated SMTP email. + +| Response | Zone | Description | +|----------|------|-------------| +| `127.0.0.10` | PBL | ISP Maintained | +| `127.0.0.11` | PBL | Spamhaus Maintained | + +#### ZEN (Combined) + +**Domain:** `zen.spamhaus.org` + +Combines all Spamhaus lists (SBL + XBL + PBL) in a single query. + +#### Error Responses + +Spamhaus returns special codes for query errors: + +| Response | Meaning | +|----------|---------| +| `127.255.255.252` | Typing error in DNSBL name | +| `127.255.255.254` | Query via public/open resolver (blocked) | +| `127.255.255.255` | Excessive number of queries (rate limited) | + +**Important:** Spamhaus limits queries from public DNS resolvers. For production use, you should: +1. Use your own recursive DNS resolver +2. Register for a free data feed (for high-volume use) + +--- + +## Use Cases + +### 1. Blocking Tor Exit Nodes + +Tor provides anonymity, but some sites need to block it: + +``` +Legitimate reasons to block: +- Prevent abuse from anonymous sources +- Regulatory compliance +- Reduce fraud from anonymous actors + +Considerations: +- You also block legitimate privacy-conscious users +- Consider CAPTCHAs or rate limiting instead of blocking +``` + +### 2. Blocking Known Spam Sources + +Using Spamhaus to block known spam sources: + +``` +Benefits: +- Block IPs with known spam history +- Reduce comment spam, form abuse +- Lower bot traffic + +Considerations: +- False positives are possible +- IP reputations can lag behind ownership changes +``` + +### 3. Blocking Compromised Machines + +The XBL lists machines infected with malware: + +``` +Benefits: +- Block botnets +- Reduce DDoS traffic +- Prevent credential stuffing + +Considerations: +- Legitimate users with infected machines get blocked +- Provide clear error messages so they know to scan their computer +``` + +--- + +## Best Practices + +### 1. Cache Results + +DNSBL queries add latency. Always use stick-table caching: + +```haproxy +# Cache for 30 minutes +backend st_cache + stick-table type ipv6 size 1m expire 30m store gpc0,gpc1 +``` + +### 2. Fail Open + +If the DNSBL is unreachable, allow traffic rather than block: + +``` +DNSBL down + fail-closed = All traffic blocked = Outage +DNSBL down + fail-open = All traffic allowed = Acceptable risk +``` + +This module implements fail-open behavior by default. + +### 3. Log DNSBL Decisions + +Always log which IPs were blocked and why: + +```haproxy +log-format "... dnsbl:%[req.hdr(X-DNSBL-Action)]" +``` + +### 4. Provide Clear Error Messages + +When blocking a user, tell them why: + +``` +401 Unauthorized +Denial-Reason: Your IP address is listed in a DNS blacklist +``` + +### 5. Use Multiple Lists Carefully + +More lists = more false positives: + +``` +Tor list alone: Low false positive risk +XBL alone: Low false positive risk +PBL: Higher false positive risk (residential IPs) +All combined: Highest false positive risk +``` + +### 6. Monitor False Positives + +Regularly review blocked requests: + +```bash +# Find blocked requests in logs +grep "DNSBL-.*-DENY" /var/log/haproxy.log | awk '{print $6}' | sort | uniq -c +``` + +--- + +## Technical Details + +### DNS Query Performance + +| Location | Typical Latency | +|----------|-----------------| +| Local DNS cache | < 1ms | +| Local recursive resolver | 1-10ms | +| ISP DNS resolver | 10-50ms | +| DNSBL server (first query) | 50-200ms | + +### IPv6 Considerations + +DNSBL queries for IPv6 are more complex: + +``` +IPv4: 192.0.2.1 → 1.2.0.192.dnsbl.example.com +IPv6: 2001:db8::1 → 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.dnsbl.example.com +``` + +Most DNSBLs only support IPv4 queries. The Tor lists, for example, only list IPv4 exit nodes. + +### Rate Limiting + +Most DNSBLs rate limit queries: + +| Provider | Free Limit | Notes | +|----------|------------|-------| +| dan.me.uk | Reasonable | No strict limit published | +| Spamhaus | ~300k/day | Commercial license for more | +| torproject.org | Reasonable | Rate limits apply | + +For high-volume use, consider: +1. Caching with long TTL +2. Commercial data feeds +3. Self-hosted mirrors (where available) + +--- + +## Security Considerations + +### DNSBL Poisoning + +A malicious actor could potentially: +- Intercept DNS responses +- Return false positives (block legitimate users) +- Return false negatives (allow malicious users) + +Mitigation: +- Use DNSSEC where available +- Use trusted, local DNS resolvers +- Don't rely solely on DNSBL for security + +### Privacy + +DNSBL queries reveal which IPs are accessing your service to the DNSBL operator. Consider: +- Running local DNSBL mirrors for sensitive applications +- The privacy trade-off vs. security benefits diff --git a/docs/http-headers.md b/docs/http-headers.md new file mode 100644 index 0000000..5232a61 --- /dev/null +++ b/docs/http-headers.md @@ -0,0 +1,307 @@ +# HTTP Headers Reference + +The HAProxy Lua DNSBL module sets several HTTP headers on each request. These headers can be used for: + +- Logging and analytics +- Downstream decision making +- Debugging +- Monitoring and alerting + +## Headers Set by dnsbl_query + +### X-DNSBL-Action + +**Description:** Indicates the result of the DNSBL lookup or cache check. + +**Type:** String + +**Possible Values:** + +| Value | Description | +|-------|-------------| +| `DNSBL-CACHE-ALLOW` | IP found in cache, previously determined to be allowed | +| `DNSBL-CACHE-DENY` | IP found in cache, previously determined to be blocked | +| `DNSBL-LOOKUP-ALLOW` | Fresh DNS lookup performed, IP not in blacklist | +| `DNSBL-LOOKUP-DENY` | Fresh DNS lookup performed, IP found in blacklist | +| `DNSBL-ERROR-ALLOW` | Error during lookup, defaulting to allow (fail-open) | + +**Example:** +```http +X-DNSBL-Action: DNSBL-LOOKUP-ALLOW +``` + +**Usage in HAProxy ACL:** +```haproxy +acl dnsbl_blocked req.hdr(X-DNSBL-Action) -m sub DENY +http-request deny if dnsbl_blocked +``` + +--- + +### X-DNSBL-Is-Allowed + +**Description:** Boolean indicator of whether the request is allowed. + +**Type:** Integer (0 or 1) + +**Values:** + +| Value | Meaning | +|-------|---------| +| `1` | Request is allowed | +| `0` | Request is blocked | + +**Example:** +```http +X-DNSBL-Is-Allowed: 1 +``` + +**Usage in HAProxy ACL:** +```haproxy +acl dnsbl_allowed req.hdr(X-DNSBL-Is-Allowed) -m str 1 +http-request deny unless dnsbl_allowed +``` + +--- + +### X-DNSBL-Version + +**Description:** Version of the DNSBL module that processed the request. + +**Type:** String (semantic version) + +**Example:** +```http +X-DNSBL-Version: 0.4.0 +``` + +**Use case:** Helpful for debugging and ensuring all HAProxy nodes run the same version. + +--- + +### X-DNSBL-Client-IP + +**Description:** The client IP address that was checked against the DNSBL. + +**Type:** String (IP address) + +**Example:** +```http +X-DNSBL-Client-IP: 192.0.2.100 +``` + +**Notes:** +- This reflects the actual IP used for the lookup +- May differ from the connection source if `src_header` or `src_var` was used +- Useful for verifying correct IP extraction when behind proxies + +--- + +### X-DNSBL-Query + +**Description:** The full DNS query string that was used for the lookup. + +**Type:** String (DNS hostname) + +**Example:** +```http +X-DNSBL-Query: 100.2.0.192.torexit.dan.me.uk +``` + +**Notes:** +- Format is `{reversed_ip}.{dnsbl_domain}` +- Useful for debugging DNS resolution issues +- Can be used to manually verify lookups with `dig` or `nslookup` + +--- + +### X-DNSBL-Error + +**Description:** Error message when the lookup fails. Only set when an error occurs. + +**Type:** String + +**Example:** +```http +X-DNSBL-Error: No entry found +``` + +**Possible Values:** +- `No entry found` - IP not in stick-table (shouldn't happen normally) +- `Unsupported stick-table type` - Stick-table type not supported +- DNS resolution errors + +--- + +### X-DNSBL-Zone (Spamhaus only) + +**Description:** The Spamhaus zone that matched. Only set for Spamhaus lookups. + +**Type:** String + +**Possible Values:** + +| Value | Full Name | +|-------|-----------| +| `SBL` | Spamhaus Block List | +| `XBL` | Exploits Block List | +| `PBL` | Policy Block List | +| `Any` | Error condition | + +**Example:** +```http +X-DNSBL-Zone: XBL +``` + +--- + +### X-DNSBL-Description (Spamhaus only) + +**Description:** Human-readable description of why the IP was blocked. Only set for Spamhaus lookups. + +**Type:** String + +**Example:** +```http +X-DNSBL-Description: CBL Data +``` + +**Possible Values:** +- `Spamhaus SBL Data` +- `Spamhaus SBL CSS Data` +- `CBL Data` +- `Spamhaus DROP/EDROP Data` +- `ISP Maintained` +- `Spamhaus Maintained` +- `Typing error in DNSBL name` +- `Query via public/open resolver` +- `Excessive number of queries` + +--- + +## Headers Set by dnsbl_block + +### Denial-Reason + +**Description:** Explanation of why the request was blocked. Only included in 401 response. + +**Type:** String + +**Example:** +```http +Denial-Reason: DNSBL: IP found in hard banlist. BLOCK request +``` + +### Server + +**Description:** Server identification in the 401 response. + +**Type:** String + +**Example:** +```http +Server: DNSBL/0.4.0 +``` + +--- + +## Using Headers in HAProxy Configuration + +### Logging Headers + +```haproxy +# Custom log format including DNSBL headers +log-format "%ci:%cp [%tr] %ft %b/%s %ST %B %{+Q}r dnsbl:%[req.hdr(X-DNSBL-Action)] ip:%[req.hdr(X-DNSBL-Client-IP)]" +``` + +### Conditional Routing Based on Headers + +```haproxy +frontend http-in + bind *:80 + + # Route blocked users to a different backend (e.g., CAPTCHA) + acl is_dnsbl_deny req.hdr(X-DNSBL-Action) -m sub DENY + use_backend captcha_servers if is_dnsbl_deny + + default_backend normal_servers +``` + +### Removing Headers Before Backend + +```haproxy +# Remove DNSBL headers before passing to backend +http-request del-header X-DNSBL-Action +http-request del-header X-DNSBL-Is-Allowed +http-request del-header X-DNSBL-Version +http-request del-header X-DNSBL-Client-IP +http-request del-header X-DNSBL-Query +http-request del-header X-DNSBL-Error +http-request del-header X-DNSBL-Zone +http-request del-header X-DNSBL-Description +``` + +### Passing Headers to Backend for Processing + +```haproxy +# Keep headers for backend processing (e.g., additional logging) +# No action needed - headers are forwarded by default + +# Backend application can access: +# - X-DNSBL-Action to know the lookup result +# - X-DNSBL-Client-IP for logging the actual client IP +``` + +--- + +## Header Summary Table + +| Header | Always Set | Type | Purpose | +|--------|------------|------|---------| +| `X-DNSBL-Action` | Yes | String | Lookup result | +| `X-DNSBL-Is-Allowed` | Yes | Integer | Allow/block indicator | +| `X-DNSBL-Version` | Yes | String | Module version | +| `X-DNSBL-Client-IP` | Yes | String | IP that was checked | +| `X-DNSBL-Query` | Yes | String | DNS query string | +| `X-DNSBL-Error` | On error | String | Error description | +| `X-DNSBL-Zone` | Spamhaus | String | Spamhaus zone | +| `X-DNSBL-Description` | Spamhaus | String | Block reason | +| `Denial-Reason` | On block | String | User-facing reason | +| `Server` | On block | String | Server identification | + +--- + +## Security Considerations + +### Header Injection + +The module sets headers based on: +- IP addresses (validated format) +- DNSBL domain (from configuration) +- DNS responses (127.x.x.x range) + +These values are controlled and not subject to header injection. + +### Information Disclosure + +Headers like `X-DNSBL-Action` reveal security decisions. Consider: + +```haproxy +# Remove sensitive headers from response to client +http-response del-header X-DNSBL-Action +http-response del-header X-DNSBL-Query +``` + +Or only forward them to the backend: + +```haproxy +# Headers are request headers, not automatically in response +# Only explicit add_header in Lua adds them to response +``` + +### Client Spoofing + +A malicious client cannot spoof these headers because: +1. They are set by the Lua module after processing +2. HAProxy `req_set_header` overwrites any existing value +3. The headers are set based on actual lookup results diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..5adb432 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,457 @@ +# Troubleshooting Guide + +This guide helps you diagnose and fix common issues with the HAProxy Lua DNSBL module. + +## Quick Diagnostics + +### Check if DNSBL Module is Loaded + +```bash +# Look for Lua loading errors in HAProxy startup +journalctl -u haproxy | grep -i lua + +# Or check HAProxy logs +grep -i "lua\|dnsbl" /var/log/haproxy.log +``` + +### Test HAProxy Configuration + +```bash +haproxy -c -f /etc/haproxy/haproxy.cfg +``` + +### Check DNSBL Headers + +```bash +curl -v http://localhost/ 2>&1 | grep -i x-dnsbl +``` + +--- + +## Common Issues + +### Issue: "lua-load: Failed to load file" + +**Symptoms:** +``` +[ALERT] ... : parsing [haproxy.cfg:5] : lua-load: Failed to load file 'dnsbl.lua' +``` + +**Causes and Solutions:** + +1. **File not found:** + ```bash + # Check if file exists + ls -la /usr/share/lua/5.3/dnsbl.lua + + # Use absolute path in config + lua-load /usr/share/lua/5.3/dnsbl.lua + ``` + +2. **Missing dependencies:** + ```bash + # Test loading in Lua directly + lua5.3 -e "require('dnsbl')" + + # If error, install missing dependencies + # For utils.lua: + wget -O /usr/share/lua/5.3/utils.lua \ + https://raw.githubusercontent.com/dobrevit/haproxy-lua-utils/main/src/utils.lua + + # For inspect.lua: + wget -O /usr/share/lua/5.3/inspect.lua \ + https://raw.githubusercontent.com/kikito/inspect.lua/master/inspect.lua + ``` + +3. **Permission issues:** + ```bash + chmod 644 /usr/share/lua/5.3/dnsbl.lua + chown root:root /usr/share/lua/5.3/dnsbl.lua + ``` + +--- + +### Issue: "No backend with stick-table" + +**Symptoms:** +``` +[DEBUG] ... : No st_dnsbl_cache backend with stick-table within +``` + +**Solutions:** + +1. **Create the backend:** + ```haproxy + backend st_dnsbl_cache + stick-table type ipv6 size 1m expire 30m store gpc0,gpc1 + ``` + +2. **Check backend name matches:** + ```haproxy + # These must match: + backend st_dnsbl_cache # ← Backend name + stick-table ... + + http-request lua.dnsbl_query st_dnsbl_cache ... # ← Same name here + ``` + +--- + +### Issue: "stick-table type 'ip' not supported" + +**Symptoms:** +``` +stick-table type 'ip' not supported. Use 'type ipv6' instead (supports both IPv4 and IPv6) +``` + +**Solution:** + +Change your stick-table type from `ip` to `ipv6`: + +```haproxy +# Before (not supported) +backend st_dnsbl_cache + stick-table type ip size 1m expire 30m store gpc0,gpc1 + +# After (correct) +backend st_dnsbl_cache + stick-table type ipv6 size 1m expire 30m store gpc0,gpc1 +``` + +**Why:** The `ipv6` type handles both IPv4 and IPv6 addresses. IPv4 addresses are automatically converted to IPv4-mapped IPv6 format (`::ffff:x.x.x.x`) internally. + +--- + +### Issue: "stktbl_lookup error: No entry found" + +**Symptoms:** +``` +[DEBUG] ... : stktbl_lookup error: No entry found. Most likely there isn't track-sc0 set +``` + +**Solution:** + +Add `track-sc0` before the DNSBL query: + +```haproxy +frontend http-in + bind *:80 + + # This line is required! + http-request track-sc0 src table st_dnsbl_cache + + http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk "" "" +``` + +--- + +### Issue: DNS Lookups Failing + +**Symptoms:** +- `X-DNSBL-Action: DNSBL-ERROR-ALLOW` on all requests +- Debug messages about DNS resolution failures + +**Diagnostics:** + +```bash +# Test DNS resolution manually +dig 1.2.0.192.torexit.dan.me.uk + +# Check if DNS is working +nslookup google.com + +# Check HAProxy's DNS settings +cat /etc/resolv.conf +``` + +**Solutions:** + +1. **Configure a working DNS resolver:** + ```bash + # /etc/resolv.conf + nameserver 8.8.8.8 + nameserver 1.1.1.1 + ``` + +2. **Use a local resolver for better performance:** + ```bash + # Install dnsmasq or unbound + apt install dnsmasq + systemctl start dnsmasq + + # Point to localhost + echo "nameserver 127.0.0.1" > /etc/resolv.conf + ``` + +--- + +### Issue: All Requests Being Blocked + +**Symptoms:** +- Every request returns 401 +- `X-DNSBL-Action: DNSBL-CACHE-DENY` for all IPs + +**Diagnostics:** + +```bash +# Check stick-table contents +echo "show table st_dnsbl_cache" | socat stdio /var/run/haproxy/admin.sock +``` + +**Solutions:** + +1. **Clear the stick-table:** + ```bash + echo "clear table st_dnsbl_cache" | socat stdio /var/run/haproxy/admin.sock + ``` + +2. **Check if your own IP is in a blacklist:** + ```bash + # Replace with your IP + dig 1.2.168.192.torexit.dan.me.uk + ``` + +3. **Verify the DNSBL domain:** + ```haproxy + # Make sure domain starts with a dot + http-request lua.dnsbl_query st_cache .torexit.dan.me.uk "" "" + # ^ dot here + ``` + +--- + +### Issue: No Requests Being Blocked + +**Symptoms:** +- Known Tor exit IPs pass through +- `X-DNSBL-Action: DNSBL-LOOKUP-ALLOW` for known blacklisted IPs + +**Diagnostics:** + +```bash +# Test with a known Tor exit node (get current list from torproject.org) +# This is an example - use a current Tor exit IP +curl -H "X-Forwarded-For: 185.220.101.1" http://localhost/ +``` + +**Solutions:** + +1. **Check if you're querying the right DNSBL:** + ```bash + # Verify the IP is actually listed + dig 1.101.220.185.torexit.dan.me.uk + # Should return 127.0.0.100 if listed + ``` + +2. **Ensure both actions are present:** + ```haproxy + http-request lua.dnsbl_query st_cache .torexit.dan.me.uk "" "" + http-request lua.dnsbl_block st_cache # ← Don't forget this! + ``` + +3. **Check if header extraction is working:** + ```bash + # Test X-Forwarded-For extraction + curl -v -H "X-Forwarded-For: 185.220.101.1" http://localhost/ 2>&1 | grep X-DNSBL-Client-IP + # Should show the X-Forwarded-For IP, not your real IP + ``` + +--- + +### Issue: Wrong IP Being Checked + +**Symptoms:** +- `X-DNSBL-Client-IP` header shows wrong IP +- Behind proxy but checking proxy IP instead of client IP + +**Solutions:** + +1. **Configure header extraction:** + ```haproxy + # Get IP from X-Forwarded-For header + http-request lua.dnsbl_query st_cache .torexit.dan.me.uk "" X-Forwarded-For + ``` + +2. **Handle multiple IPs in X-Forwarded-For:** + ```haproxy + # Extract first IP only + http-request set-var(txn.real_ip) hdr(X-Forwarded-For),word(1,",") + http-request lua.dnsbl_query st_cache .torexit.dan.me.uk txn.real_ip "" + ``` + +3. **Track the correct IP in stick-table:** + ```haproxy + # Track header IP, not socket IP + http-request track-sc0 hdr(X-Forwarded-For) table st_cache + ``` + +--- + +### Issue: Spamhaus Rate Limiting + +**Symptoms:** +- `X-DNSBL-Description: Excessive number of queries` +- `X-DNSBL-Description: Query via public/open resolver` + +**Solutions:** + +1. **Use your own recursive resolver:** + ```bash + # Don't use public DNS (8.8.8.8, 1.1.1.1) for Spamhaus + # Set up local resolver + apt install unbound + ``` + +2. **Increase cache TTL:** + ```haproxy + backend st_spam_cache + stick-table type ipv6 size 1m expire 1h store gpc0,gpc1 + # ^^^^ longer expiry + ``` + +3. **Consider Spamhaus Data Query Service:** + - For high-volume use, register at spamhaus.org + +--- + +## Enabling Debug Logging + +The module has commented debug statements. To enable them: + +1. **Edit dnsbl.lua:** + ```lua + -- Change lines like: + --txn:Debug(string.format("DNSBL: client IP: %s\n", client_ip)) + + -- To: + txn:Debug(string.format("DNSBL: client IP: %s\n", client_ip)) + ``` + +2. **Reload HAProxy:** + ```bash + systemctl reload haproxy + ``` + +3. **View debug output:** + ```bash + journalctl -u haproxy -f + ``` + +--- + +## Stick-Table Commands + +### View Table Contents + +```bash +echo "show table st_dnsbl_cache" | socat stdio /var/run/haproxy/admin.sock +``` + +Output format: +``` +# table: st_dnsbl_cache, type: ipv6, size:1048576, used:2 +0x1234567890: key=::ffff:192.0.2.1 use=0 exp=1800000 gpc0=1 gpc1=0 +0x1234567891: key=::ffff:198.51.100.1 use=0 exp=1800000 gpc0=0 gpc1=1 +``` + +### Clear Specific Entry + +```bash +echo "clear table st_dnsbl_cache key ::ffff:192.0.2.1" | socat stdio /var/run/haproxy/admin.sock +``` + +### Clear Entire Table + +```bash +echo "clear table st_dnsbl_cache" | socat stdio /var/run/haproxy/admin.sock +``` + +### Set Entry Manually (for testing) + +```bash +# Set gpc0 (allow counter) +echo "set table st_dnsbl_cache key ::ffff:192.0.2.1 data.gpc0 1" | socat stdio /var/run/haproxy/admin.sock +``` + +--- + +## Testing DNSBL Lookups Manually + +### Dan.me.uk Tor List + +```bash +# Format: reversed_ip.torexit.dan.me.uk +dig 1.101.220.185.torexit.dan.me.uk + +# Listed response: 127.0.0.100 +# Not listed: NXDOMAIN +``` + +### Tor Project List + +```bash +dig 1.101.220.185.80.exitlist.torproject.org + +# Listed response: 127.0.0.2 +# Not listed: NXDOMAIN +``` + +### Spamhaus XBL + +```bash +dig 1.101.220.185.xbl.spamhaus.org + +# Various 127.0.0.x responses +# Not listed: NXDOMAIN +``` + +--- + +## Performance Issues + +### Slow First Requests + +**Cause:** DNS lookup latency + +**Solutions:** +1. Use a local caching DNS resolver +2. Increase stick-table expiry time +3. Pre-warm cache with known IPs + +### High Memory Usage + +**Cause:** Large stick-table + +**Solutions:** +1. Reduce stick-table size: + ```haproxy + stick-table type ipv6 size 500k expire 15m store gpc0,gpc1 + ``` + +2. Use shorter expiry time + +### High CPU Usage + +**Cause:** Too many DNS lookups (cache misses) + +**Solutions:** +1. Increase stick-table size (more cache hits) +2. Increase expiry time +3. Batch similar IPs (e.g., /24 networks) - requires code modification + +--- + +## Getting Help + +If you're still having issues: + +1. **Check GitHub Issues:** https://github.com/dobrevit/haproxy-lua-dnsbl/issues + +2. **Open a new issue** with: + - HAProxy version (`haproxy -v`) + - Lua version (`lua -v`) + - Relevant HAProxy configuration (sanitized) + - Debug log output + - Steps to reproduce + +3. **HAProxy mailing list:** For general HAProxy questions diff --git a/examples/basic/README.md b/examples/basic/README.md new file mode 100644 index 0000000..6a5e787 --- /dev/null +++ b/examples/basic/README.md @@ -0,0 +1,63 @@ +# Basic DNSBL Example + +This is the simplest possible DNSBL configuration for HAProxy. + +## What This Does + +- Blocks Tor exit nodes using the dan.me.uk DNSBL +- Uses direct client IP (no proxy in front) +- Caches results for 30 minutes + +## Prerequisites + +1. HAProxy with Lua support +2. DNSBL module installed at `/usr/share/lua/5.3/dnsbl.lua` +3. Dependencies: `utils.lua`, `socket`, `inspect.lua` + +## Usage + +```bash +# Test configuration syntax +haproxy -c -f haproxy.cfg + +# Run HAProxy +haproxy -f haproxy.cfg + +# Test with curl +curl -v http://localhost/ +``` + +## Verification + +Check the response headers: + +```bash +curl -s -D - http://localhost/ -o /dev/null | grep X-DNSBL +``` + +Expected output for non-Tor IP: +``` +X-DNSBL-Action: DNSBL-LOOKUP-ALLOW +X-DNSBL-Is-Allowed: 1 +X-DNSBL-Version: 0.4.0 +X-DNSBL-Client-IP: 127.0.0.1 +X-DNSBL-Query: 1.0.0.127.torexit.dan.me.uk +``` + +## Configuration Breakdown + +```haproxy +# Cache DNSBL results +backend st_dnsbl_cache + stick-table type ipv6 size 1m expire 30m store gpc0,gpc1 + +frontend http-in + # Track IPs (required for caching) + http-request track-sc0 src table st_dnsbl_cache + + # Perform lookup + http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk "" "" + + # Block if blacklisted + http-request lua.dnsbl_block st_dnsbl_cache +``` diff --git a/examples/basic/haproxy.cfg b/examples/basic/haproxy.cfg new file mode 100644 index 0000000..d7e1140 --- /dev/null +++ b/examples/basic/haproxy.cfg @@ -0,0 +1,59 @@ +# HAProxy Lua DNSBL - Basic Configuration Example +# +# This is a minimal working configuration that demonstrates +# DNSBL-based blocking of Tor exit nodes. +# +# Usage: +# haproxy -f haproxy.cfg + +global + log stdout format raw local0 info + + # Load the DNSBL Lua module + # Adjust the path to match your installation + lua-load /usr/share/lua/5.3/dnsbl.lua + +defaults + log global + mode http + option httplog + + timeout connect 5s + timeout client 30s + timeout server 30s + +# Stick-table backend for caching DNSBL results +# +# - type ipv6: Supports both IPv4 and IPv6 addresses +# - size 1m: Cache up to 1 million IP addresses +# - expire 30m: Cache entries expire after 30 minutes +# - store gpc0,gpc1: gpc0=allowed, gpc1=blocked +backend st_dnsbl_cache + stick-table type ipv6 size 1m expire 30m store gpc0,gpc1 + +# Your backend servers +backend webservers + balance roundrobin + server web1 127.0.0.1:8080 check + +# Main frontend with DNSBL protection +frontend http-in + bind *:80 + + # Step 1: Track the source IP in the stick-table + # This MUST come before dnsbl_query + http-request track-sc0 src table st_dnsbl_cache + + # Step 2: Perform DNSBL lookup + # Parameters: + # 1. st_dnsbl_cache - Backend with stick-table + # 2. .torexit.dan.me.uk - DNSBL domain to query + # 3. "" - No custom variable for IP (empty) + # 4. "" - No header for IP (empty, use direct connection) + http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk "" "" + + # Step 3: Block requests from blacklisted IPs + http-request lua.dnsbl_block st_dnsbl_cache + + # Route allowed requests to backend + default_backend webservers diff --git a/examples/behind-proxy/README.md b/examples/behind-proxy/README.md new file mode 100644 index 0000000..daacca5 --- /dev/null +++ b/examples/behind-proxy/README.md @@ -0,0 +1,65 @@ +# Behind Proxy DNSBL Example + +When HAProxy is behind another reverse proxy (nginx, CDN, load balancer), you need to extract the real client IP from an HTTP header. + +## Common Headers + +| Proxy Type | Header | +|------------|--------| +| Most proxies | `X-Forwarded-For` | +| nginx (when configured) | `X-Real-IP` | +| Cloudflare | `CF-Connecting-IP` | +| AWS ALB | `X-Forwarded-For` | +| Akamai | `True-Client-IP` | + +## Configuration Examples + +### Simple X-Forwarded-For + +```haproxy +http-request track-sc0 hdr(X-Forwarded-For) table st_dnsbl_cache +http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk "" X-Forwarded-For +``` + +### Multiple Proxies (X-Forwarded-For: client, proxy1, proxy2) + +```haproxy +# Extract first IP only +http-request set-var(txn.real_ip) hdr(X-Forwarded-For),word(1,",") +http-request track-sc0 var(txn.real_ip) table st_dnsbl_cache +http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk txn.real_ip "" +``` + +### Cloudflare + +```haproxy +http-request track-sc0 hdr(CF-Connecting-IP) table st_dnsbl_cache +http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk "" CF-Connecting-IP +``` + +## Testing + +```bash +# Simulate request from behind proxy +curl -H "X-Forwarded-For: 192.0.2.100" http://localhost/ + +# Check which IP was used +curl -s -D - -H "X-Forwarded-For: 192.0.2.100" http://localhost/ -o /dev/null | grep X-DNSBL-Client-IP +# Should show: X-DNSBL-Client-IP: 192.0.2.100 +``` + +## Security Considerations + +**Warning:** When using headers for client IP, ensure: + +1. Only trusted proxies can reach HAProxy directly +2. Proxies are configured to set/overwrite the header (not append) +3. Consider validating the header format + +```haproxy +# Only trust internal proxy IPs +acl is_trusted_proxy src 10.0.0.0/8 192.168.0.0/16 + +# Reject requests that claim to be forwarded but aren't from trusted proxies +http-request deny if !is_trusted_proxy { req.hdr(X-Forwarded-For) -m found } +``` diff --git a/examples/behind-proxy/haproxy.cfg b/examples/behind-proxy/haproxy.cfg new file mode 100644 index 0000000..59527b6 --- /dev/null +++ b/examples/behind-proxy/haproxy.cfg @@ -0,0 +1,96 @@ +# HAProxy Lua DNSBL - Behind Proxy Configuration +# +# This configuration is for when HAProxy sits behind another +# reverse proxy (nginx, CDN, load balancer, etc.) and needs +# to extract the real client IP from a header. +# +# Common scenarios: +# - Behind nginx with proxy_set_header X-Forwarded-For +# - Behind AWS ALB/ELB +# - Behind Cloudflare +# - Behind any CDN + +global + log stdout format raw local0 info + lua-load /usr/share/lua/5.3/dnsbl.lua + +defaults + log global + mode http + option httplog + option forwardfor + + timeout connect 5s + timeout client 30s + timeout server 30s + +# Stick-table for caching +backend st_dnsbl_cache + stick-table type ipv6 size 1m expire 30m store gpc0,gpc1 + +# Backend servers +backend webservers + balance roundrobin + server web1 127.0.0.1:8080 check + +# Frontend - Using X-Forwarded-For Header +frontend http-in + bind *:80 + + # Option 1: Simple - Get first IP from X-Forwarded-For + # Track the header value in stick-table + http-request track-sc0 hdr(X-Forwarded-For) table st_dnsbl_cache + + # Use the header for DNSBL lookup + http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk "" X-Forwarded-For + + http-request lua.dnsbl_block st_dnsbl_cache + + default_backend webservers + + +# Alternative Frontend - Using X-Real-IP Header +# (Common with nginx) +frontend http-in-realip + bind *:8081 + + http-request track-sc0 hdr(X-Real-IP) table st_dnsbl_cache + + http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk "" X-Real-IP + + http-request lua.dnsbl_block st_dnsbl_cache + + default_backend webservers + + +# Alternative Frontend - Handle Multiple IPs in X-Forwarded-For +# When there are multiple proxies: "client, proxy1, proxy2" +frontend http-in-multi-proxy + bind *:8082 + + # Extract only the first (leftmost) IP from X-Forwarded-For + http-request set-var(txn.real_ip) hdr(X-Forwarded-For),word(1,",") + + # Track the extracted IP + http-request track-sc0 var(txn.real_ip) table st_dnsbl_cache + + # Use the variable for lookup + http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk txn.real_ip "" + + http-request lua.dnsbl_block st_dnsbl_cache + + default_backend webservers + + +# Alternative Frontend - Cloudflare with CF-Connecting-IP +frontend http-in-cloudflare + bind *:8083 + + # Cloudflare uses CF-Connecting-IP for the real client IP + http-request track-sc0 hdr(CF-Connecting-IP) table st_dnsbl_cache + + http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk "" CF-Connecting-IP + + http-request lua.dnsbl_block st_dnsbl_cache + + default_backend webservers diff --git a/examples/docker/README.md b/examples/docker/README.md new file mode 100644 index 0000000..871afc0 --- /dev/null +++ b/examples/docker/README.md @@ -0,0 +1,118 @@ +# Docker DNSBL Example + +A complete Docker-based test environment for HAProxy Lua DNSBL. + +## Quick Start + +```bash +# Build and start +docker-compose up -d + +# Test +curl http://localhost:8080/ + +# Check DNSBL headers +curl -v http://localhost:8080/ 2>&1 | grep X-DNSBL + +# View logs +docker-compose logs -f haproxy + +# Stop +docker-compose down +``` + +## Components + +| Service | Port | Description | +|---------|------|-------------| +| haproxy | 8080 | HAProxy with DNSBL module | +| backend | 80 | Simple nginx backend | + +## Testing DNSBL + +### Test with a spoofed IP (simulating proxy) + +```bash +# Test with a non-Tor IP +curl -H "X-Forwarded-For: 1.2.3.4" http://localhost:8080/ + +# Test with a Tor exit IP (get current from torproject.org) +curl -H "X-Forwarded-For: 185.220.101.1" http://localhost:8080/ +``` + +### Check stick-table + +```bash +# View cached entries +docker exec haproxy-dnsbl echo "show table st_dnsbl_cache" | socat stdio /var/run/haproxy/admin.sock + +# Clear cache +docker exec haproxy-dnsbl echo "clear table st_dnsbl_cache" | socat stdio /var/run/haproxy/admin.sock +``` + +## File Structure + +``` +docker/ +├── docker-compose.yml # Docker Compose configuration +├── haproxy/ +│ ├── Dockerfile # HAProxy with Lua modules +│ └── haproxy.cfg # HAProxy configuration +└── README.md # This file +``` + +## Building + +```bash +# Rebuild after changes +docker-compose build --no-cache + +# Or build just haproxy +docker-compose build haproxy +``` + +## Customization + +### Change DNSBL provider + +Edit `haproxy/haproxy.cfg`: + +```haproxy +# For Spamhaus instead of Tor list +http-request lua.dnsbl_query st_dnsbl_cache xbl.spamhaus.org "" X-Forwarded-For +``` + +### Use different Lua modules + +Add to `haproxy/Dockerfile`: + +```dockerfile +RUN wget -O /usr/share/lua/5.3/mymodule.lua https://example.com/mymodule.lua +``` + +## Troubleshooting + +### DNS not working inside container + +Check DNS configuration: + +```bash +docker exec haproxy-dnsbl cat /etc/resolv.conf +docker exec haproxy-dnsbl dig google.com +``` + +### Lua module errors + +Check HAProxy logs: + +```bash +docker-compose logs haproxy | grep -i lua +``` + +### Permission issues + +Ensure files have correct permissions: + +```bash +chmod 644 haproxy/haproxy.cfg +``` diff --git a/examples/docker/backend/html/index.html b/examples/docker/backend/html/index.html new file mode 100644 index 0000000..9b4d818 --- /dev/null +++ b/examples/docker/backend/html/index.html @@ -0,0 +1,64 @@ + + + + DNSBL Test Backend + + + +
+

HAProxy Lua DNSBL Test

+

Request allowed through DNSBL filter.

+ +

How to Test

+
+

Check DNSBL headers:

+ curl -v http://localhost:8080/ 2>&1 | grep X-DNSBL + +

Simulate Tor exit IP:

+ curl -H "X-Forwarded-For: 185.220.101.1" http://localhost:8080/ +
+ +

Expected Headers

+
    +
  • X-DNSBL-Action - CACHE-ALLOW, LOOKUP-ALLOW, etc.
  • +
  • X-DNSBL-Is-Allowed - 1 (allowed) or 0 (blocked)
  • +
  • X-DNSBL-Client-IP - Your IP address
  • +
  • X-DNSBL-Query - DNS query made
  • +
+
+ + diff --git a/examples/docker/docker-compose.yml b/examples/docker/docker-compose.yml new file mode 100644 index 0000000..8286591 --- /dev/null +++ b/examples/docker/docker-compose.yml @@ -0,0 +1,35 @@ +version: '3.8' + +services: + # HAProxy with DNSBL Lua module + haproxy: + build: + context: ./haproxy + dockerfile: Dockerfile + container_name: haproxy-dnsbl + ports: + - "8080:80" + depends_on: + - backend + networks: + - dnsbl-network + # Enable DNS resolution for DNSBL queries + dns: + - 8.8.8.8 + - 1.1.1.1 + restart: unless-stopped + + # Simple backend server for testing + backend: + image: nginx:alpine + container_name: backend-server + networks: + - dnsbl-network + # Simple response page + volumes: + - ./backend/html:/usr/share/nginx/html:ro + restart: unless-stopped + +networks: + dnsbl-network: + driver: bridge diff --git a/examples/docker/haproxy/Dockerfile b/examples/docker/haproxy/Dockerfile new file mode 100644 index 0000000..66763a0 --- /dev/null +++ b/examples/docker/haproxy/Dockerfile @@ -0,0 +1,38 @@ +# HAProxy with Lua DNSBL Module +FROM haproxy:2.8-alpine + +# Install Lua and socket library +RUN apk add --no-cache \ + lua5.3 \ + lua5.3-socket \ + curl \ + socat + +# Create Lua modules directory +RUN mkdir -p /usr/share/lua/5.3 + +# Download dependencies +WORKDIR /usr/share/lua/5.3 + +# Download utils.lua (haproxy-lua-utils) +RUN curl -sL -o utils.lua \ + https://raw.githubusercontent.com/dobrevit/haproxy-lua-utils/main/src/utils.lua + +# Download inspect.lua (for debugging) +RUN curl -sL -o inspect.lua \ + https://raw.githubusercontent.com/kikito/inspect.lua/master/inspect.lua + +# Copy DNSBL module (from parent directory in build context) +COPY dnsbl.lua /usr/share/lua/5.3/dnsbl.lua + +# Copy HAProxy configuration +COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg + +# Create socket directory for stats +RUN mkdir -p /var/run/haproxy + +# Verify configuration +RUN haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg + +# Default command +CMD ["haproxy", "-f", "/usr/local/etc/haproxy/haproxy.cfg"] diff --git a/examples/docker/haproxy/dnsbl.lua b/examples/docker/haproxy/dnsbl.lua new file mode 100644 index 0000000..2537fdf --- /dev/null +++ b/examples/docker/haproxy/dnsbl.lua @@ -0,0 +1,346 @@ +-- The MIT License (MIT) +-- +-- Copyright (c) 2023 Dobrev IT Ltd., Martin Dobrev +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy +-- of this software and associated documentation files (the "Software"), to deal +-- in the Software without restriction, including without limitation the rights +-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +-- copies of the Software, and to permit persons to whom the Software is +-- furnished to do so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- +-- SPDX-License-Identifier: MIT +-- +-- Description: DNSBL query and block action +-- Version: 0.4.0 +-- +-- This is a Haproxy Lua action that prepares a DNSBL query and +-- performs a DNSBL lookup. If the DNSBL lookup returns a positive +-- result, the block action will block the request. +-- +-- Supported DNSBL providers: +-- - .torexit.dan.me.uk (Tor exit nodes, response: 127.0.0.100) +-- - .exitlist.torproject.org (Tor exit nodes, response: 127.0.0.2) +-- - *.spamhaus.org (Spamhaus lists, various responses) +-- + + +local _M={} +local utils = require("utils") +local socket = require("socket") +local inspect = require("inspect") + +_M.version = "0.4.0" + +_M.stktbl_lookup = function(stktbl, key) + local st_info = stktbl:info() + if st_info.type == "ipv6" then + if key:find(".", 1, true) then + key = '::ffff:' .. key + end + elseif st_info.type == "ip" then + return nil, "stick-table type 'ip' not supported. Use 'type ipv6' instead (supports both IPv4 and IPv6)" + elseif st_info.type == "string" then + return nil, "stick-table type 'string' not supported. Use 'type ipv6' instead" + else + return nil, "Unsupported stick-table type: " .. tostring(st_info.type) .. ". Use 'type ipv6' instead" + end + + local entry = stktbl:lookup(key) + --txn:Debug(string.format("stktbl_lookup result: %s", inspect(entry))) + if entry then + return entry + else + return nil, "No entry found" + end +end + +_M.spamhaus_response = function(response) + local spamhaus_response_map = { + ["127.255.255.252"] = { + ["zone"] = "Any", + ["description"] = "Typing error in DNSBL name", + ["permitted"] = false + }, + ["127.255.255.254"] = { + ["zone"] = "Any", + ["description"] = "Query via public/open resolver", + ["permitted"] = false + }, + ["127.255.255.255"] = { + ["zone"] = "Any", + ["description"] = "Excessive number of queries", + ["permitted"] = false + }, + ["127.0.0.2"] = { + ["zone"] = "SBL", + ["description"] = "Spamhaus SBL Data", + ["permitted"] = false + }, + ["127.0.0.3"] = { + ["zone"] = "SBL", + ["description"] = "Spamhaus SBL CSS Data", + ["permitted"] = false + }, + ["127.0.0.4"] = { + ["zone"] = "XBL", + ["description"] = "CBL Data", + ["permitted"] = false + }, + ["127.0.0.9"] = { + ["zone"] = "SBL", + ["description"] = "Spamhaus DROP/EDROP Data (in addition to 127.0.0.2, since 01-Jun-2016)", + ["permitted"] = false + }, + ["127.0.0.10"] = { + ["zone"] = "PBL", + ["description"] = "ISP Maintained", + ["permitted"] = false + }, + ["127.0.0.11"] = { + ["zone"] = "PBL", + ["description"] = "Spamhaus Maintained", + ["permitted"] = false + } + } + + if spamhaus_response_map[response] then + local zone = spamhaus_response_map[response]["zone"] + local description = spamhaus_response_map[response]["description"] + local permitted = spamhaus_response_map[response]["permitted"] + return permitted, zone, description + else + return true, nil, nil + end +end + +-- Check if a DNSBL response indicates the IP should be blocked +-- Returns: blocked (boolean), zone (string or nil), description (string or nil) +_M.is_blocked_response = function(response, bl_domain) + -- Tor exit list from dan.me.uk + if bl_domain:find("torexit.dan.me.uk", 1, true) then + if response == "127.0.0.100" then + return true, "TOR", "Dan.me.uk Tor Exit Node" + end + return false, nil, nil + end + + -- Tor exit list from Tor Project + if bl_domain:find("exitlist.torproject.org", 1, true) then + if response == "127.0.0.2" then + return true, "TOR", "Tor Project Exit Node" + end + return false, nil, nil + end + + -- Spamhaus lists (sbl, xbl, pbl, zen, etc.) + if bl_domain:find("spamhaus", 1, true) then + local permitted, zone, description = _M.spamhaus_response(response) + if not permitted then + return true, zone, description + end + return false, nil, nil + end + + -- Default: any response in 127.x.x.x range is considered blocked + if response and response:find("^127%.") then + return true, "UNKNOWN", "Unknown DNSBL response: " .. response + end + + return false, nil, nil +end + +-- Helper functions for configurable track-sc index +-- These allow using sc0, sc1, or sc2 based on configuration + +_M.sc_inc_gpc0 = function(txn, backend, sc_index) + sc_index = sc_index or 0 + if sc_index == 0 then + return txn.f:sc0_inc_gpc0(backend) + elseif sc_index == 1 then + return txn.f:sc1_inc_gpc0(backend) + elseif sc_index == 2 then + return txn.f:sc2_inc_gpc0(backend) + else + return txn.f:sc0_inc_gpc0(backend) + end +end + +_M.sc_inc_gpc1 = function(txn, backend, sc_index) + sc_index = sc_index or 0 + if sc_index == 0 then + return txn.f:sc0_inc_gpc1(backend) + elseif sc_index == 1 then + return txn.f:sc1_inc_gpc1(backend) + elseif sc_index == 2 then + return txn.f:sc2_inc_gpc1(backend) + else + return txn.f:sc0_inc_gpc1(backend) + end +end + +function dnsbl_query(txn, st_name, bl_domain, src_var, src_header, sc_index) + -- configuration + local st_dnsbl_cache = st_name + local is_new_visitor = false + local is_allowed = false + local client_ip = txn:get_var("txn.dnsbl_client_ip") + + -- Parse sc_index (default to 0 if not provided or empty) + if sc_index == nil or sc_index == "" then + sc_index = 0 + else + sc_index = tonumber(sc_index) or 0 + end + + -- get client IP from the source variable (if set) + if not utils.is_nil(src_var) then + client_ip = txn:get_var(src_var) + -- check if src_var exists and is not empty + if not utils.is_nil(client_ip) then + --txn:Debug(string.format("DNSBL: client IP: %s\n", client_ip)) + txn:set_var("txn.dnsbl_client_ip", client_ip) + end + end + + if utils.is_nil(client_ip) and not utils.is_nil(src_header) then + client_ip = txn.sf:req_hdr(src_header) + if not utils.is_nil(client_ip) then + --txn:Debug(string.format("DNSBL: client IP: %s\n", client_ip)) + txn:set_var("txn.dnsbl_client_ip", client_ip) + end + end + + if utils.is_nil(client_ip) then + client_ip = txn.sf:src() + --txn:Debug(string.format("DNSBL: client IP: %s\n", client_ip)) + txn:set_var("txn.dnsbl_client_ip", client_ip) + end + --txn:Debug(string.format("DNSBL: client IP: %s\n", client_ip)) + + local reverse_client_ip, err = utils.reverse_ip(client_ip) + + if not reverse_client_ip then + txn:Debug(string.format("Error reversing IP: %s\n", err)) + return false + end + + local query = string.format("%s.%s", reverse_client_ip, bl_domain) + + if core.backends[st_dnsbl_cache] and core.backends[st_dnsbl_cache].stktable then + local st = core.backends[st_dnsbl_cache].stktable + local st_lookup, err = _M.stktbl_lookup(st, client_ip) + if not st_lookup then + txn:Debug(string.format("stktbl_lookup error: %s. Most likely there isn't track-sc0 set in your HAProxy configuration", err)) + txn.http:req_set_header("X-DNSBL-Action", "DNSBL-ERROR-ALLOW") + txn.http:req_set_header("X-DNSBL-Error", err) + is_allowed = true + else + --txn:Debug(string.format("st_known_visitors stick-table lookup for %s: %s", client_ip, inspect(st_lookup))) + if st_lookup.gpc0 == 1 then + --txn:Debug(string.format("DNSBL: IP %s found in %s cache. ALLOW access", client_ip, bl_domain)) + txn.http:req_set_header("X-DNSBL-Action", "DNSBL-CACHE-ALLOW") + is_allowed = true + elseif st_lookup.gpc1 == 1 then + --txn:Debug(string.format("DNSBL: IP %s found in %s cache. DENY access", client_ip, bl_domain)) + txn.http:req_set_header("X-DNSBL-Action", "DNSBL-CACHE-DENY") + elseif st_lookup.gpc0 == 0 or st_lookup.gpc1 == 0 then + --txn:Debug(string.format("DNSBL: IP %s not previously seen in %s cache. LOOKUP required", client_ip, bl_domain)) + is_new_visitor = true + end + end + else + txn:Debug(string.format("No %s backend with stick-table within", st_dnsbl_cache)) + end + + if is_new_visitor then + local ip, details = socket.dns.toip(query) + if not ip then + if details and details ~= '"host not found"' then + --txn:Debug(string.format("DNSBL: IP %s not found in %s. Details: %s. ALLOW access", client_ip, bl_domain, inspect(details))) + txn.http:req_set_header("X-DNSBL-Action", "DNSBL-LOOKUP-ALLOW") + _M.sc_inc_gpc0(txn, st_dnsbl_cache, sc_index) + is_allowed = true + else + txn:Debug("dnsbl_query: DNS resolution failed: " .. inspect(details)) + return false + end + else + -- Check if this DNSBL response indicates a blocked IP + local blocked, zone, description = _M.is_blocked_response(ip, bl_domain) + if blocked then + --txn:Debug(string.format("DNSBL: IP %s found in %s. DENY access", client_ip, bl_domain)) + txn.http:req_set_header("X-DNSBL-Action", "DNSBL-LOOKUP-DENY") + _M.sc_inc_gpc1(txn, st_dnsbl_cache, sc_index) + -- Set additional headers for Spamhaus zone/description + if zone then + txn.http:req_set_header("X-DNSBL-Zone", zone) + end + if description then + txn.http:req_set_header("X-DNSBL-Description", description) + end + else + --txn:Debug(string.format("DNSBL: Response %s for %s not in block list. ALLOW access", ip, bl_domain)) + txn.http:req_set_header("X-DNSBL-Action", "DNSBL-LOOKUP-ALLOW") + _M.sc_inc_gpc0(txn, st_dnsbl_cache, sc_index) + is_allowed = true + end + end + end + + if is_allowed then + txn.http:req_set_header("X-DNSBL-Is-Allowed", 1) + else + txn.http:req_set_header("X-DNSBL-Is-Allowed", 0) + end + + txn.http:req_set_header("X-DNSBL-Version", _M.version) + txn.http:req_set_header("X-DNSBL-Client-IP", client_ip) + txn.http:req_set_header("X-DNSBL-Query", query) + + txn:set_var("txn.dnsbl_is_allowed", is_allowed) +end + +function dnsbl_block_banned(txn, st_name) + local client_ip = txn:get_var("txn.dnsbl_client_ip") + local is_allowed = txn:get_var("txn.dnsbl_is_allowed") + local st = core.backends[st_name].stktable + local st_lookup, err = _M.stktbl_lookup(st, client_ip) + + --txn:Debug(string.format("DNSBL: st_lookup: %s", inspect(st_lookup))) + + if is_allowed == "false" then + if not st_lookup then + --txn:Debug(string.format("stktbl_lookup error: %s", err)) + return false + else + if st_lookup.gpc0 > 1 then + --txn:Debug(string.format("DNSBL: IP %s found in %s ban-list. BLOCK request", client_ip, st_name)) + local reply = txn:reply() + reply:set_status(401, "Unauthorized") + reply:add_header("Content-Type", "text/html") + reply:add_header("Server", string.format("DNSBL/%s", _M.version)) + reply:add_header("Denial-Reason", "DNSBL: IP found in hard banlist. BLOCK request") + txn:done(reply) + end + end + else + return false + end +end + +core.register_action("dnsbl_query", {"http-req"}, dnsbl_query, 5) +core.register_action("dnsbl_block", {"http-req"}, dnsbl_block_banned, 1) + +return _M diff --git a/examples/docker/haproxy/haproxy.cfg b/examples/docker/haproxy/haproxy.cfg new file mode 100644 index 0000000..a335e90 --- /dev/null +++ b/examples/docker/haproxy/haproxy.cfg @@ -0,0 +1,60 @@ +# HAProxy Lua DNSBL - Docker Example Configuration +# +# This configuration is designed for the Docker example environment. + +global + log stdout format raw local0 info + + # Load the DNSBL Lua module + lua-load /usr/share/lua/5.3/dnsbl.lua + + # Stats socket for debugging + stats socket /var/run/haproxy/admin.sock mode 660 level admin + +defaults + log global + mode http + option httplog + option forwardfor + + timeout connect 5s + timeout client 30s + timeout server 30s + +# Stick-table for caching DNSBL results +backend st_dnsbl_cache + stick-table type ipv6 size 100k expire 30m store gpc0,gpc1 + +# Backend servers (nginx container) +backend webservers + balance roundrobin + server backend backend:80 check + +# Main frontend with DNSBL protection +frontend http-in + bind *:80 + + # Track source IP in stick-table + # Using X-Forwarded-For for when behind another proxy + http-request set-var(txn.client_ip) hdr(X-Forwarded-For),word(1,",") if { hdr(X-Forwarded-For) -m found } + http-request set-var(txn.client_ip) src if !{ var(txn.client_ip) -m found } + + http-request track-sc0 var(txn.client_ip) table st_dnsbl_cache + + # DNSBL lookup using Tor exit list + http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk txn.client_ip "" + + # Block blacklisted IPs + http-request lua.dnsbl_block st_dnsbl_cache + + # Custom log format with DNSBL info + log-format "%ci [%t] %ft %b/%s %ST %B %r dnsbl:%[req.hdr(X-DNSBL-Action)] ip:%[req.hdr(X-DNSBL-Client-IP)]" + + default_backend webservers + +# Stats page (optional, for debugging) +frontend stats + bind *:8404 + stats enable + stats uri /stats + stats refresh 10s diff --git a/examples/logging/README.md b/examples/logging/README.md new file mode 100644 index 0000000..4fbc595 --- /dev/null +++ b/examples/logging/README.md @@ -0,0 +1,78 @@ +# DNSBL Logging Examples + +This example demonstrates various logging configurations for monitoring DNSBL activity. + +## Log Format Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `%[req.hdr(X-DNSBL-Action)]` | Lookup result | `DNSBL-LOOKUP-ALLOW` | +| `%[req.hdr(X-DNSBL-Is-Allowed)]` | Boolean | `1` or `0` | +| `%[req.hdr(X-DNSBL-Client-IP)]` | Checked IP | `192.0.2.100` | +| `%[req.hdr(X-DNSBL-Query)]` | DNS query | `100.2.0.192.torexit.dan.me.uk` | +| `%[req.hdr(X-DNSBL-Version)]` | Module version | `0.4.0` | + +## Example Log Outputs + +### Standard Format +``` +192.0.2.1:54321 [14/Jan/2024:12:00:00.000] http-in webservers/web1 ... dnsbl_action:DNSBL-LOOKUP-ALLOW dnsbl_ip:192.0.2.1 dnsbl_allowed:1 +``` + +### JSON Format +```json +{ + "timestamp": "14/Jan/2024:12:00:00.000", + "client_ip": "192.0.2.1", + "dnsbl": { + "action": "DNSBL-LOOKUP-DENY", + "client_ip": "185.220.101.1", + "allowed": "0" + } +} +``` + +### Minimal Format +``` +[14/Jan/2024:12:00:00.000] 185.220.101.1 DNSBL-LOOKUP-DENY GET / HTTP/1.1 +``` + +## Log Analysis Commands + +### Count DNSBL actions +```bash +grep -oP 'dnsbl_action:\K[^ ]+' /var/log/haproxy.log | sort | uniq -c | sort -rn +``` + +### Find blocked IPs +```bash +grep 'DENY' /var/log/haproxy.log | grep -oP 'dnsbl_ip:\K[^ ]+' | sort | uniq -c | sort -rn +``` + +### Cache hit ratio +```bash +awk '/dnsbl_action:DNSBL-CACHE/ {cache++} /dnsbl_action:DNSBL-LOOKUP/ {lookup++} END {print "Cache:", cache, "Lookup:", lookup, "Hit%:", cache/(cache+lookup)*100}' /var/log/haproxy.log +``` + +## Monitoring + +### Prometheus/Grafana +Use HAProxy's built-in stats with DNSBL log parsing: + +```bash +# Example: count blocked requests per minute +grep 'DNSBL-.*-DENY' /var/log/haproxy.log | \ + awk '{print substr($0, 1, 20)}' | \ + uniq -c +``` + +### Alerting +Alert on high block rates: + +```bash +# Alert if > 100 blocks in last 5 minutes +COUNT=$(grep -c 'DENY' /var/log/haproxy.log) +if [ $COUNT -gt 100 ]; then + echo "High DNSBL block rate: $COUNT" +fi +``` diff --git a/examples/logging/haproxy.cfg b/examples/logging/haproxy.cfg new file mode 100644 index 0000000..1c61a84 --- /dev/null +++ b/examples/logging/haproxy.cfg @@ -0,0 +1,94 @@ +# HAProxy Lua DNSBL - Logging Configuration +# +# This configuration demonstrates how to log DNSBL decisions +# and create useful log formats for monitoring and analysis. + +global + log stdout format raw local0 info + lua-load /usr/share/lua/5.3/dnsbl.lua + +defaults + log global + mode http + option httplog + + timeout connect 5s + timeout client 30s + timeout server 30s + +# Stick-table for caching +backend st_dnsbl_cache + stick-table type ipv6 size 1m expire 30m store gpc0,gpc1 + +# Backend servers +backend webservers + balance roundrobin + server web1 127.0.0.1:8080 check + +# Frontend with comprehensive logging +frontend http-in + bind *:80 + + http-request track-sc0 src table st_dnsbl_cache + http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk "" "" + http-request lua.dnsbl_block st_dnsbl_cache + + # Custom log format with DNSBL information + # Available variables from DNSBL headers: + # - %[req.hdr(X-DNSBL-Action)] - CACHE-ALLOW, LOOKUP-DENY, etc. + # - %[req.hdr(X-DNSBL-Is-Allowed)] - 1 or 0 + # - %[req.hdr(X-DNSBL-Client-IP)] - IP that was checked + # - %[req.hdr(X-DNSBL-Query)] - DNS query made + + log-format "%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Ta %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r dnsbl_action:%[req.hdr(X-DNSBL-Action)] dnsbl_ip:%[req.hdr(X-DNSBL-Client-IP)] dnsbl_allowed:%[req.hdr(X-DNSBL-Is-Allowed)]" + + default_backend webservers + + +# Alternative: JSON log format for structured logging +frontend http-in-json + bind *:8081 + + http-request track-sc0 src table st_dnsbl_cache + http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk "" "" + http-request lua.dnsbl_block st_dnsbl_cache + + # JSON log format for log aggregators (ELK, Splunk, etc.) + log-format '{"timestamp":"%t","client_ip":"%ci","client_port":%cp,"frontend":"%ft","backend":"%b","server":"%s","status":%ST,"bytes":%B,"request":"%r","user_agent":"%[req.hdr(User-Agent)]","dnsbl":{"action":"%[req.hdr(X-DNSBL-Action)]","client_ip":"%[req.hdr(X-DNSBL-Client-IP)]","allowed":"%[req.hdr(X-DNSBL-Is-Allowed)]","query":"%[req.hdr(X-DNSBL-Query)]"}}' + + default_backend webservers + + +# Alternative: Minimal log format focusing on DNSBL +frontend http-in-minimal + bind *:8082 + + http-request track-sc0 src table st_dnsbl_cache + http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk "" "" + http-request lua.dnsbl_block st_dnsbl_cache + + # Simple format: timestamp, IP, action, request + log-format "[%t] %[req.hdr(X-DNSBL-Client-IP)] %[req.hdr(X-DNSBL-Action)] %r" + + default_backend webservers + + +# Alternative: Separate log for DNSBL events only +frontend http-in-separate-log + bind *:8083 + + http-request track-sc0 src table st_dnsbl_cache + http-request lua.dnsbl_query st_dnsbl_cache .torexit.dan.me.uk "" "" + + # ACL to detect blocked requests + acl dnsbl_denied req.hdr(X-DNSBL-Action) -m sub DENY + + # Log DENY events to a separate log + http-request set-var(txn.log_dnsbl) str(1) if dnsbl_denied + + http-request lua.dnsbl_block st_dnsbl_cache + + # Standard log format + log-format "%ci:%cp [%t] %ft %b/%s %ST %B %r" + + default_backend webservers diff --git a/examples/multi-dnsbl/README.md b/examples/multi-dnsbl/README.md new file mode 100644 index 0000000..57a5d83 --- /dev/null +++ b/examples/multi-dnsbl/README.md @@ -0,0 +1,124 @@ +# Multiple DNSBL Providers Example + +This example shows how to check client IPs against multiple DNS blacklist providers. + +## Providers + +| Provider | Domain | Blocks | +|----------|--------|--------| +| Dan.me.uk | `.torexit.dan.me.uk` | Tor exit nodes | +| Tor Project | `.exitlist.torproject.org` | Tor exit nodes | +| Spamhaus XBL | `xbl.spamhaus.org` | Exploits/botnets | +| Spamhaus ZEN | `zen.spamhaus.org` | Combined list | + +## Critical: Matching track-sc and sc_index + +When using multiple DNSBL providers with separate stick-tables, the `sc_index` parameter **must match** the `track-sc` index used for each table: + +```haproxy +# track-sc0 → sc_index 0 +# track-sc1 → sc_index 1 +# track-sc2 → sc_index 2 + +http-request track-sc0 src table st_tor +http-request track-sc1 src table st_spam + +http-request lua.dnsbl_query st_tor .torexit.dan.me.uk "" "" 0 # matches track-sc0 +http-request lua.dnsbl_query st_spam xbl.spamhaus.org "" "" 1 # matches track-sc1 +``` + +If `sc_index` doesn't match, the gpc counters will be incremented on the wrong stick-table entry, and caching will not work correctly. + +## Strategy Options + +### 1. Block All Matches + +Use separate stick-tables and block from all: + +```haproxy +http-request track-sc0 src table st_tor_dan +http-request track-sc1 src table st_tor_project +http-request track-sc2 src table st_spamhaus + +http-request lua.dnsbl_query st_tor_dan .torexit.dan.me.uk "" "" 0 +http-request lua.dnsbl_query st_tor_project .exitlist.torproject.org "" "" 1 +http-request lua.dnsbl_query st_spamhaus xbl.spamhaus.org "" "" 2 + +http-request lua.dnsbl_block st_tor_dan +http-request lua.dnsbl_block st_tor_project +http-request lua.dnsbl_block st_spamhaus +``` + +### 2. Block Some, Log Others + +Only block Tor, but log Spamhaus matches: + +```haproxy +http-request track-sc0 src table st_tor +http-request track-sc1 src table st_spam + +http-request lua.dnsbl_query st_tor .torexit.dan.me.uk "" "" 0 +http-request lua.dnsbl_query st_spam xbl.spamhaus.org "" "" 1 + +# Only block Tor +http-request lua.dnsbl_block st_tor +# Spamhaus headers logged but not blocked +``` + +### 3. Custom Handling with ACLs + +```haproxy +acl is_tor req.hdr(X-DNSBL-Action) -m sub DENY + +# Redirect Tor users +http-request redirect location /tor-notice.html if is_tor +``` + +## Considerations + +### Performance +- Each DNSBL adds a potential DNS lookup +- Use caching (stick-tables) to minimize lookups +- Consider longer TTLs for stable lists (Tor lists) + +### False Positives +- More lists = more potential false positives +- Spamhaus PBL has higher false positive rate +- Monitor and adjust based on your needs + +### Stick-Table Design + +**Separate tables (recommended):** +```haproxy +backend st_tor # For Tor lists +backend st_spam # For Spamhaus + +# Remember: each table needs its own track-sc index and matching sc_index parameter +http-request track-sc0 src table st_tor +http-request track-sc1 src table st_spam +http-request lua.dnsbl_query st_tor .torexit.dan.me.uk "" "" 0 +http-request lua.dnsbl_query st_spam xbl.spamhaus.org "" "" 1 +``` + +**Single table (simpler but less granular):** +```haproxy +backend st_dnsbl # Shared for all + +# Only one track-sc needed, sc_index can be omitted (defaults to 0) +http-request track-sc0 src table st_dnsbl +http-request lua.dnsbl_query st_dnsbl .torexit.dan.me.uk "" "" +http-request lua.dnsbl_query st_dnsbl xbl.spamhaus.org "" "" +# Note: Last lookup result wins in cache (both queries update the same gpc counters) +``` + +**Important:** When using separate tables with different `track-sc` indices, you **must** pass the corresponding `sc_index` parameter to `dnsbl_query`. Without it, all queries default to `sc0`, which means only the first table's counters get updated correctly. This will cause caching to fail for the other tables. + +## Testing + +```bash +# Check headers from all lookups +curl -v http://localhost/ 2>&1 | grep X-DNSBL + +# Note: Headers are overwritten by each dnsbl_query +# Only the last lookup's headers will be visible +``` diff --git a/examples/multi-dnsbl/haproxy.cfg b/examples/multi-dnsbl/haproxy.cfg new file mode 100644 index 0000000..3fa7d26 --- /dev/null +++ b/examples/multi-dnsbl/haproxy.cfg @@ -0,0 +1,119 @@ +# HAProxy Lua DNSBL - Multiple DNSBL Providers Configuration +# +# This configuration demonstrates using multiple DNSBL providers +# to check IPs against different blacklists. +# +# Providers used: +# - dan.me.uk Tor exit list +# - Tor Project exit list +# - Spamhaus XBL (exploit sources) + +global + log stdout format raw local0 info + lua-load /usr/share/lua/5.3/dnsbl.lua + +defaults + log global + mode http + option httplog + + timeout connect 5s + timeout client 30s + timeout server 30s + +# Separate stick-tables for each DNSBL provider +# This allows different cache policies per provider +backend st_tor_dan + stick-table type ipv6 size 500k expire 1h store gpc0,gpc1 + +backend st_tor_project + stick-table type ipv6 size 500k expire 1h store gpc0,gpc1 + +backend st_spamhaus + stick-table type ipv6 size 1m expire 30m store gpc0,gpc1 + +# Backend servers +backend webservers + balance roundrobin + server web1 127.0.0.1:8080 check + +# Frontend checking all DNSBL providers +frontend http-in + bind *:80 + + # Track IPs in all stick-tables + # Using different track-sc indices for each table + http-request track-sc0 src table st_tor_dan + http-request track-sc1 src table st_tor_project + http-request track-sc2 src table st_spamhaus + + # Check all DNSBL providers + # IMPORTANT: sc_index parameter must match the track-sc index for each table + http-request lua.dnsbl_query st_tor_dan .torexit.dan.me.uk "" "" 0 + http-request lua.dnsbl_query st_tor_project .exitlist.torproject.org "" "" 1 + http-request lua.dnsbl_query st_spamhaus xbl.spamhaus.org "" "" 2 + + # Block if any DNSBL matched + # Note: This checks all tables sequentially + http-request lua.dnsbl_block st_tor_dan + http-request lua.dnsbl_block st_tor_project + http-request lua.dnsbl_block st_spamhaus + + default_backend webservers + + +# Alternative: Block only Tor, log but allow Spamhaus matches +frontend http-in-tor-only + bind *:8081 + + http-request track-sc0 src table st_tor_dan + http-request track-sc1 src table st_spamhaus + + # sc_index must match track-sc index + http-request lua.dnsbl_query st_tor_dan .torexit.dan.me.uk "" "" 0 + http-request lua.dnsbl_query st_spamhaus xbl.spamhaus.org "" "" 1 + + # Only block Tor exit nodes + http-request lua.dnsbl_block st_tor_dan + + # Spamhaus matches are logged but not blocked + # (headers will show DNSBL-LOOKUP-DENY for Spamhaus matches) + + default_backend webservers + + +# Alternative: Use ACLs for custom blocking logic +frontend http-in-acl + bind *:8082 + + http-request track-sc0 src table st_tor_dan + http-request track-sc1 src table st_spamhaus + + # sc_index must match track-sc index + http-request lua.dnsbl_query st_tor_dan .torexit.dan.me.uk "" "" 0 + http-request lua.dnsbl_query st_spamhaus xbl.spamhaus.org "" "" 1 + + # Define ACLs based on DNSBL results + acl is_tor_exit req.hdr(X-DNSBL-Action) -m sub DENY + + # Custom handling: redirect Tor users to a different page + http-request redirect location /tor-detected.html if is_tor_exit + + default_backend webservers + + +# Alternative: Single stick-table, multiple DNSBLs (simpler but less granular) +frontend http-in-simple + bind *:8083 + + http-request track-sc0 src table st_tor_dan + + # Multiple queries, same stick-table + # Note: Results will overwrite each other in the cache + # Last lookup result is what gets cached + http-request lua.dnsbl_query st_tor_dan .torexit.dan.me.uk "" "" + http-request lua.dnsbl_query st_tor_dan xbl.spamhaus.org "" "" + + http-request lua.dnsbl_block st_tor_dan + + default_backend webservers diff --git a/src/dnsbl.lua b/src/dnsbl.lua index 131eb7b..2f24308 100644 --- a/src/dnsbl.lua +++ b/src/dnsbl.lua @@ -23,12 +23,17 @@ -- SPDX-License-Identifier: MIT -- -- Description: DNSBL query and block action --- Version: 0.2 +-- Version: 0.4.0 -- -- This is a Haproxy Lua action that prepares a DNSBL query and -- performs a DNSBL lookup. If the DNSBL lookup returns a positive -- result, the block action will block the request. -- +-- Supported DNSBL providers: +-- - .torexit.dan.me.uk (Tor exit nodes, response: 127.0.0.100) +-- - .exitlist.torproject.org (Tor exit nodes, response: 127.0.0.2) +-- - *.spamhaus.org (Spamhaus lists, various responses) +-- local _M={} @@ -36,7 +41,7 @@ local utils = require("utils") local socket = require("socket") local inspect = require("inspect") -_M.version = "0.3.0" +_M.version = "0.4.0" _M.stktbl_lookup = function(stktbl, key) local st_info = stktbl:info() @@ -45,11 +50,11 @@ _M.stktbl_lookup = function(stktbl, key) key = '::ffff:' .. key end elseif st_info.type == "ip" then - -- not yet implemented + return nil, "stick-table type 'ip' not supported. Use 'type ipv6' instead (supports both IPv4 and IPv6)" elseif st_info.type == "string" then - -- not yet implemented + return nil, "stick-table type 'string' not supported. Use 'type ipv6' instead" else - return nil, "Unsupported stick-table type" + return nil, "Unsupported stick-table type: " .. tostring(st_info.type) .. ". Use 'type ipv6' instead" end local entry = stktbl:lookup(key) @@ -120,12 +125,84 @@ _M.spamhaus_response = function(response) end end -function dnsbl_query(txn, st_name, bl_domain, src_var, src_header) +-- Check if a DNSBL response indicates the IP should be blocked +-- Returns: blocked (boolean), zone (string or nil), description (string or nil) +_M.is_blocked_response = function(response, bl_domain) + -- Tor exit list from dan.me.uk + if bl_domain:find("torexit.dan.me.uk", 1, true) then + if response == "127.0.0.100" then + return true, "TOR", "Dan.me.uk Tor Exit Node" + end + return false, nil, nil + end + + -- Tor exit list from Tor Project + if bl_domain:find("exitlist.torproject.org", 1, true) then + if response == "127.0.0.2" then + return true, "TOR", "Tor Project Exit Node" + end + return false, nil, nil + end + + -- Spamhaus lists (sbl, xbl, pbl, zen, etc.) + if bl_domain:find("spamhaus", 1, true) then + local permitted, zone, description = _M.spamhaus_response(response) + if not permitted then + return true, zone, description + end + return false, nil, nil + end + + -- Default: any response in 127.x.x.x range is considered blocked + if response and response:find("^127%.") then + return true, "UNKNOWN", "Unknown DNSBL response: " .. response + end + + return false, nil, nil +end + +-- Helper functions for configurable track-sc index +-- These allow using sc0, sc1, or sc2 based on configuration + +_M.sc_inc_gpc0 = function(txn, backend, sc_index) + sc_index = sc_index or 0 + if sc_index == 0 then + return txn.f:sc0_inc_gpc0(backend) + elseif sc_index == 1 then + return txn.f:sc1_inc_gpc0(backend) + elseif sc_index == 2 then + return txn.f:sc2_inc_gpc0(backend) + else + return txn.f:sc0_inc_gpc0(backend) + end +end + +_M.sc_inc_gpc1 = function(txn, backend, sc_index) + sc_index = sc_index or 0 + if sc_index == 0 then + return txn.f:sc0_inc_gpc1(backend) + elseif sc_index == 1 then + return txn.f:sc1_inc_gpc1(backend) + elseif sc_index == 2 then + return txn.f:sc2_inc_gpc1(backend) + else + return txn.f:sc0_inc_gpc1(backend) + end +end + +function dnsbl_query(txn, st_name, bl_domain, src_var, src_header, sc_index) -- configuration local st_dnsbl_cache = st_name local is_new_visitor = false local is_allowed = false local client_ip = txn:get_var("txn.dnsbl_client_ip") + + -- Parse sc_index (default to 0 if not provided or empty) + if sc_index == nil or sc_index == "" then + sc_index = 0 + else + sc_index = tonumber(sc_index) or 0 + end -- get client IP from the source variable (if set) if not utils.is_nil(src_var) then @@ -193,19 +270,31 @@ function dnsbl_query(txn, st_name, bl_domain, src_var, src_header) if details and details ~= '"host not found"' then --txn:Debug(string.format("DNSBL: IP %s not found in %s. Details: %s. ALLOW access", client_ip, bl_domain, inspect(details))) txn.http:req_set_header("X-DNSBL-Action", "DNSBL-LOOKUP-ALLOW") - txn.f:sc0_inc_gpc0(st_dnsbl_cache) + _M.sc_inc_gpc0(txn, st_dnsbl_cache, sc_index) is_allowed = true else txn:Debug("dnsbl_query: DNS resolution failed: " .. inspect(details)) return false end else - if ip == "127.0.0.100" then + -- Check if this DNSBL response indicates a blocked IP + local blocked, zone, description = _M.is_blocked_response(ip, bl_domain) + if blocked then --txn:Debug(string.format("DNSBL: IP %s found in %s. DENY access", client_ip, bl_domain)) txn.http:req_set_header("X-DNSBL-Action", "DNSBL-LOOKUP-DENY") - txn.f:sc0_inc_gpc1(st_dnsbl_cache) + _M.sc_inc_gpc1(txn, st_dnsbl_cache, sc_index) + -- Set additional headers for Spamhaus zone/description + if zone then + txn.http:req_set_header("X-DNSBL-Zone", zone) + end + if description then + txn.http:req_set_header("X-DNSBL-Description", description) + end else - txn:Debug(string.format("DNSBL: Unknown response %s. Details: %s", ips, inspect(details))) + --txn:Debug(string.format("DNSBL: Response %s for %s not in block list. ALLOW access", ip, bl_domain)) + txn.http:req_set_header("X-DNSBL-Action", "DNSBL-LOOKUP-ALLOW") + _M.sc_inc_gpc0(txn, st_dnsbl_cache, sc_index) + is_allowed = true end end end @@ -251,7 +340,7 @@ function dnsbl_block_banned(txn, st_name) end end -core.register_action("dnsbl_query", {"http-req"}, dnsbl_query, 4) +core.register_action("dnsbl_query", {"http-req"}, dnsbl_query, 5) core.register_action("dnsbl_block", {"http-req"}, dnsbl_block_banned, 1) return _M