Creating Nginx Modules in C: Real-World Examples for GNU/Linux

Creating Nginx Modules in C: Real-World Examples for GNU/Linux

Ever tried to bend Nginx to your will and ended up banging your head against its config files? What if I told you that you can extend Nginx’s core with blazing-fast C modules that slot right into its event-driven, asynchronous engine? Today, we’re diving deep into creating real-world Nginx modules in C, showing you how to supercharge your server with custom logic without sacrificing performance or stability.

Let’s rip open the hood and see how Nginx modules work, how to write one from scratch, and how to deploy it on a GNU/Linux system. Ready to level up your server game? 🚀


Why Write Nginx Modules in C?

Nginx is a beast for handling massive concurrent connections with minimal resources. Config files and Lua scripts are great for many cases, but sometimes you need raw power and flexibility — that’s where C modules come in.

  • Performance: Native C code runs blazingly fast, no JIT or interpreter overhead.
  • Full access: Hook into Nginx internals like request processing, upstream handling, and event loops.
  • Customization: Implement custom protocols, authentication schemes, or content transformations.

But beware: writing Nginx modules is yak-shaving territory. You’ll wrestle with its internal data structures, lifecycle hooks, and memory pools. Let’s break it down so you don’t shoot yourself in the foot.


Nginx Module Anatomy: The Basics

Think of an Nginx module like a plugin with a few key parts:

  • Module Context: Defines callbacks for config parsing, initialization, and request handling.
  • Directives: Custom config options your module exposes.
  • Handlers: Functions triggered at various phases of request processing (e.g., content phase).
  • Module Definition: The core struct that ties everything together.

The MMU of Your Module: Memory Pools

Nginx uses its own memory pool system to avoid malloc/free overhead. Your module gets passed an ngx_pool_t * pointer to allocate short-lived memory tied to request lifetimes. Forget malloc here — use ngx_palloc and friends.


Hello, World! A Minimal Content Handler Module

Let’s start with the classic “Hello, World!” served by Nginx from a custom module.

#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>

// The handler function called during the content phase
static ngx_int_t ngx_http_hello_handler(ngx_http_request_t *r) {
    // Only respond to GET and HEAD
    if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) {
        return NGX_HTTP_NOT_ALLOWED;
    }

    // Discard request body, if any
    ngx_int_t rc = ngx_http_discard_request_body(r);
    if (rc != NGX_OK) {
        return rc;
    }

    // Set Content-Type header
    ngx_str_t type = ngx_string("text/plain");
    r->headers_out.content_type = type;

    // Prepare response body
    ngx_str_t response = ngx_string("Hello, Nginx Module World!\n");

    // Set Content-Length
    r->headers_out.content_length_n = response.len;
    r->headers_out.status = NGX_HTTP_OK;

    // Send headers
    rc = ngx_http_send_header(r);
    if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
        return rc;
    }

    // Allocate buffer for response body
    ngx_buf_t *b = ngx_create_temp_buf(r->pool, response.len);
    if (b == NULL) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    // Copy response data to buffer
    ngx_memcpy(b->pos, response.data, response.len);
    b->last = b->pos + response.len;
    b->last_buf = 1; // This is the last buffer in the response

    // Create a chain link for the buffer
    ngx_chain_t out;
    out.buf = b;
    out.next = NULL;

    // Send the response body
    return ngx_http_output_filter(r, &out);
}

// Hook into the content phase for our custom location
static char *ngx_http_hello(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) {
    ngx_http_core_loc_conf_t *clcf;
    // Get core location conf to set handler
    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
    clcf->handler = ngx_http_hello_handler;
    return NGX_CONF_OK;
}

// Define our module directives
static ngx_command_t ngx_http_hello_commands[] = {
    {
        ngx_string("hello_world"), // Directive name
        NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS, // Location context, no args
        ngx_http_hello, // Set handler function
        0, 0, NULL
    },
    ngx_null_command
};

// Module context (no config needed here)
static ngx_http_module_t ngx_http_hello_module_ctx = {
    NULL, // preconfiguration
    NULL, // postconfiguration
    NULL, // create main config
    NULL, // init main config
    NULL, // create server config
    NULL, // merge server config
    NULL, // create location config
    NULL  // merge location config
};

// Module definition
ngx_module_t ngx_http_hello_module = {
    NGX_MODULE_V1,
    &ngx_http_hello_module_ctx, // module context
    ngx_http_hello_commands,    // module directives
    NGX_HTTP_MODULE,            // module type
    NULL, NULL, NULL, NULL, NULL, NULL, NGX_MODULE_V1_PADDING
};

What’s Happening Here?

  • We define a directive hello_world you can put inside any location block.
  • When Nginx hits that location, it runs our ngx_http_hello_handler.
  • The handler builds a simple plain-text response using Nginx’s buffer and chain API.
  • We carefully set content-type, length, and status headers before sending the body.

Real-World Example: A Simple Auth Module

Say you want to restrict access based on a static token header (like a poor-man’s API key). Here’s a stripped-down example:

#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>

static ngx_int_t ngx_http_token_auth_handler(ngx_http_request_t *r) {
    ngx_table_elt_t *h = NULL;
    ngx_list_part_t *part = &r->headers_in.headers.part;
    ngx_table_elt_t *header = part->elts;

    // Look for "X-Auth-Token" header
    for (ngx_uint_t i = 0; /* void */; i++) {
        if (i >= part->nelts) {
            if (part->next == NULL) {
                break;
            }
            part = part->next;
            header = part->elts;
            i = 0;
        }
        if (ngx_strcasecmp(header[i].key.data, (u_char *)"X-Auth-Token") == 0) {
            h = &header[i];
            break;
        }
    }

    if (h == NULL || ngx_strcmp(h->value.data, (u_char *)"SuperSecretToken") != 0) {
        return NGX_HTTP_FORBIDDEN;
    }

    return NGX_DECLINED; // Let other handlers continue
}

static ngx_int_t ngx_http_token_auth_init(ngx_conf_t *cf) {
    ngx_http_core_main_conf_t *cmcf;
    cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

    // Insert our handler into the preaccess phase
    ngx_http_handler_pt *h = ngx_array_push(&cmcf->phases[NGX_HTTP_PREACCESS_PHASE].handlers);
    if (h == NULL) {
        return NGX_ERROR;
    }
    *h = ngx_http_token_auth_handler;

    return NGX_OK;
}

static ngx_http_module_t ngx_http_token_auth_module_ctx = {
    NULL,
    ngx_http_token_auth_init, // postconfiguration hook
    NULL, NULL, NULL, NULL, NULL, NULL
};

ngx_module_t ngx_http_token_auth_module = {
    NGX_MODULE_V1,
    &ngx_http_token_auth_module_ctx,
    NULL,
    NGX_HTTP_MODULE,
    NULL, NULL, NULL, NULL, NULL, NULL, NGX_MODULE_V1_PADDING
};

How This Works

  • We scan request headers for X-Auth-Token.
  • If missing or wrong, return 403 Forbidden early in the preaccess phase.
  • Otherwise, we decline and let Nginx continue processing.

Building and Loading Your Module

1. Compile Your Module as a Shared Object

Assuming Nginx source is in /usr/local/src/nginx-1.25.0 and your module is ngx_http_hello_module.c:

cd /usr/local/src/nginx-1.25.0
./configure --with-compat --add-dynamic-module=/path/to/your/module
make modules

This produces objs/ngx_http_hello_module.so.

2. Load the Module in Nginx Config

Add this to your nginx.conf top-level:

load_module modules/ngx_http_hello_module.so;

3. Use Your Directive

server {
    listen 8080;

    location /hello {
        hello_world;
    }
}

Reload Nginx and hit http://localhost:8080/hello — your C-powered response should light up your terminal.


TL;DR

  • Nginx modules in C hook into request phases with custom handlers.
  • Use Nginx’s memory pools and buffer chains for efficient response generation.
  • Define directives to link config to your code.
  • Build with --add-dynamic-module and load with load_module.
  • Real-world modules can do auth, logging, protocol tweaks, and more — all blazing fast.

Mic Drop 💥

Writing Nginx modules in C is like wielding a double-edged sword: insanely powerful but demands respect. Next time you hit a config wall or need a custom feature at the kernel of your web stack, why not dive in and craft your own module? What’s the wildest thing you’d build inside Nginx? Drop your ideas below — let’s hack the web server beast together! ⚙️🔥