强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第09章:VMOD 扩展开发

第09章:VMOD 扩展开发

9.1 VMOD 概述

VMOD(Varnish Module)是 Varnish 的扩展模块机制,允许通过 C 语言编写自定义功能并集成到 VCL 中。

9.1.1 VMOD 的作用

功能说明
扩展 VCL 功能实现 VCL 原生不支持的功能
性能优化C 实现比 VCL 逻辑更快
系统集成与外部系统(数据库、缓存)交互
工具函数提供字符串、日期、加密等工具函数

9.1.2 官方 VMOD 列表

VMOD功能说明
std标准库基础工具函数
directors负载均衡Director 管理
cookieCookie 操作Cookie 解析和操作
header头部操作HTTP 头部处理
tcpTCP 操作TCP 连接控制
vtc测试支持测试辅助函数
blob二进制数据Blob 数据处理
digest摘要算法MD5/SHA 等哈希
geoip2地理位置GeoIP2 数据库查询
bodyaccess请求体访问读取请求体
saintmode圣人模式故障后端隔离
xkey软清除基于 key 的缓存清除

9.2 使用官方 VMOD

9.2.1 std VMOD

vcl 4.1;
import std;

sub vcl_recv {
    # 字符串操作
    set req.http.X-Lower = std.tolower(req.http.Host);
    set req.http.X-Upper = std.toupper(req.url);
    set req.http.X-Length = std.integer(req.http.Content-Length, 0);

    # 时间操作
    set req.http.X-Time = std.time("2026-01-01T00:00:00", now);
    set req.http.X-Duration = std.duration("300s", 0s);

    # 随机数
    set req.http.X-Random = std.random(1, 100);

    # 日志
    std.log("Request received: " + req.url);

    # IP 操作
    set req.http.X-IP = std.ip(req.http.X-Forwarded-For, "0.0.0.0");

    # 后端健康检查
    if (!std.healthy(req.backend_hint)) {
        set req.http.X-Backend-Status = "unhealthy";
    }

    # 文件操作
    if (std.file_exists("/tmp/maintenance")) {
        return (synth(503, "Maintenance mode"));
    }

    # 整数转字符串
    set req.http.X-Port = std.port(server.ip);
}

sub vcl_backend_response {
    # 时间解析
    set beresp.http.X-Start-Time = std.time(beresp.http.X-Start-Time, now);

    # 持续时间
    set beresp.http.X-TTL-Seconds = std.duration(beresp.ttl, 0s);
}
vcl 4.1;
import cookie;

sub vcl_recv {
    # 解析请求中的 Cookie
    cookie.parse(req.http.Cookie);

    # 获取特定 Cookie 值
    set req.http.X-Session = cookie.get("session_id");
    set req.http.X-User = cookie.get("user_id");

    # 检查 Cookie 是否存在
    if (cookie.isset("logged_in")) {
        set req.http.X-Logged-In = "true";
    }

    # 清理 Cookie,只保留需要的
    cookie.keep("session_id,user_id,csrf_token");
    set req.http.Cookie = cookie.get_string();

    # 删除特定 Cookie
    cookie.delete("tracking_id");
    set req.http.Cookie = cookie.get_string();
}

sub vcl_backend_response {
    # 设置 Cookie
    if (beresp.http.Set-Cookie ~ "session_id=") {
        # 标记为会话 Cookie
        cookie.parse(beresp.http.Set-Cookie);
    }
}

9.2.3 digest VMOD

vcl 4.1;
import digest;

sub vcl_recv {
    # MD5 哈希
    set req.http.X-MD5 = digest.md5(req.url);

    # SHA256 哈希
    set req.http.X-SHA256 = digest.sha256(req.url);

    # HMAC 签名
    set req.http.X-HMAC = digest.hmac_sha256("secret-key", req.url);

    # Base64 编码
    set req.http.X-Base64 = digest.base64_encode(req.url);

    # Base64 解码
    set req.http.X-Decoded = digest.base64_decode(req.http.X-Base64);
}

sub vcl_deliver {
    # 生成 ETag
    set resp.http.ETag = "\"" + digest.md5(resp.http.Content-Length + obj.last_modified) + "\"";
}

9.2.4 header VMOD

vcl 4.1;
import header;

sub vcl_recv {
    # 获取头部值
    set req.http.X-Accept = header.get(req.http.Accept, "text/html");

    # 追加头部值
    header.append(req.http.X-Custom, "value1");
    header.append(req.http.X-Custom, "value2");

    # 头部存在检查
    if (header.exists(req.http.Authorization)) {
        set req.http.X-Auth = "present";
    }
}

9.2.5 xkey VMOD(软清除)

vcl 4.1;
import xkey;

acl purge_allowed {
    "localhost";
    "192.168.0.0"/24;
}

sub vcl_recv {
    # 使用 xkey 进行软清除
    if (req.method == "PURGE") {
        if (!client.ip ~ purge_allowed) {
            return (synth(403, "Forbidden"));
        }

        if (req.http.xkey) {
            # 按照 key 清除
            set req.http.n-purged = xkey.purge(req.http.xkey);
            return (synth(200, "Purged " + req.http.n-purged + " objects"));
        }
    }
}

sub vcl_backend_response {
    # 设置缓存 key
    if (beresp.http.X-Cache-Key) {
        xkey.add(beresp.http.X-Cache-Key);
    }
}

# 使用示例:
# curl -X PURGE -H "xkey: product-123" http://localhost:6081/
# 这会清除所有带有 product-123 key 的缓存对象

9.3 自定义 VMOD 开发

9.3.1 开发环境准备

# Ubuntu/Debian
sudo apt-get install -y \
    varnish-dev \
    python3 \
    python3-docutils \
    automake \
    autoconf \
    libtool \
    pkg-config

# RHEL/CentOS
sudo dnf install -y \
    varnish-devel \
    python3 \
    python3-docutils \
    automake \
    autoconf \
    libtool \
    pkgconfig

9.3.2 VMOD 项目结构

my-vmod/
├── src/
│   ├── vmod_my_module.c    # C 源码
│   └── vmod_my_module.vcc  # VCC 定义文件
├── autogen.des             # 自动生成脚本
├── configure.ac            # Autoconf 配置
├── Makefile.am             # Automake 配置
└── README.md

9.3.3 VCC 定义文件

# src/vmod_my_module.vcc

$Module my_module 3 "My Custom VMOD"

# 函数声明
$Function STRING to_upper(STRING str)
$Function STRING to_lower(STRING str)
$Function STRING md5_hash(STRING input)
$Function INT random_range(INT min, INT max)
$Function BOOL is_valid_email(STRING email)
$Function STRING get_env(STRING name, STRING default)

# 子程序声明
$Function VOID init_session(PRIV_TASK session)
$Function STRING get_session_id(PRIV_TASK session)

9.3.4 C 源码实现

// src/vmod_my_module.c

#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <openssl/md5.h>

#include "vdef.h"
#include "vrt.h"
#include "vre.h"
#include "vas.h"
#include "vcl.h"

// 字符串转大写
VCL_STRING
vmod_to_upper(VRT_CTX, VCL_STRING str)
{
    char *result;
    size_t len;
    int i;

    CHECK_OBJ_NOTNULL(ctx, VRT_CTX_MAGIC);

    if (str == NULL)
        return (NULL);

    len = strlen(str);
    result = WS_Alloc(ctx->ws, len + 1);
    if (result == NULL)
        return (NULL);

    for (i = 0; i < len; i++)
        result[i] = toupper(str[i]);
    result[len] = '\0';

    return (result);
}

// 字符串转小写
VCL_STRING
vmod_to_lower(VRT_CTX, VCL_STRING str)
{
    char *result;
    size_t len;
    int i;

    CHECK_OBJ_NOTNULL(ctx, VRT_CTX_MAGIC);

    if (str == NULL)
        return (NULL);

    len = strlen(str);
    result = WS_Alloc(ctx->ws, len + 1);
    if (result == NULL)
        return (NULL);

    for (i = 0; i < len; i++)
        result[i] = tolower(str[i]);
    result[len] = '\0';

    return (result);
}

// MD5 哈希
VCL_STRING
vmod_md5_hash(VRT_CTX, VCL_STRING input)
{
    unsigned char digest[MD5_DIGEST_LENGTH];
    char *result;
    int i;

    CHECK_OBJ_NOTNULL(ctx, VRT_CTX_MAGIC);

    if (input == NULL)
        return (NULL);

    MD5((unsigned char *)input, strlen(input), digest);

    result = WS_Alloc(ctx->ws, MD5_DIGEST_LENGTH * 2 + 1);
    if (result == NULL)
        return (NULL);

    for (i = 0; i < MD5_DIGEST_LENGTH; i++)
        sprintf(result + (i * 2), "%02x", digest[i]);

    return (result);
}

// 随机数范围
VCL_INT
vmod_random_range(VRT_CTX, VCL_INT min, VCL_INT max)
{
    CHECK_OBJ_NOTNULL(ctx, VRT_CTX_MAGIC);
    return (min + rand() % (max - min + 1));
}

// 邮箱验证
VCL_BOOL
vmod_is_valid_email(VRT_CTX, VCL_STRING email)
{
    CHECK_OBJ_NOTNULL(ctx, VRT_CTX_MAGIC);

    if (email == NULL)
        return (0);

    // 简单验证:包含 @ 和 .
    if (strchr(email, '@') == NULL || strchr(email, '.') == NULL)
        return (0);

    return (1);
}

// 获取环境变量
VCL_STRING
vmod_get_env(VRT_CTX, VCL_STRING name, VCL_STRING default_val)
{
    const char *value;

    CHECK_OBJ_NOTNULL(ctx, VRT_CTX_MAGIC);

    value = getenv(name);
    if (value == NULL)
        return (default_val);

    return (VRT_INT_string(ctx, value));
}

9.3.5 编译和安装 VMOD

# 生成配置文件
./autogen.des

# 配置
./configure

# 编译
make

# 安装
sudo make install

# 验证安装
varnishadm vmod.list

9.3.6 在 VCL 中使用自定义 VMOD

vcl 4.1;

import my_module;

sub vcl_recv {
    # 使用自定义 VMOD 函数
    set req.http.X-Upper = my_module.to_upper(req.url);
    set req.http.X-Hash = my_module.md5_hash(req.url);

    if (!my_module.is_valid_email(req.http.X-Email)) {
        return (synth(400, "Invalid email"));
    }
}

9.4 VMOD 开发最佳实践

9.4.1 内存管理

// 使用 Varnish 工作空间(推荐)
char *result = WS_Alloc(ctx->ws, size);

// 复制字符串到工作空间
char *result = WS_Printf(ctx->ws, "format: %s", value);

// 检查工作空间溢出
AN(WS_Allocated(ctx->ws, result));

9.4.2 错误处理

VCL_STRING
vmod_my_function(VRT_CTX, VCL_STRING input)
{
    CHECK_OBJ_NOTNULL(ctx, VRT_CTX_MAGIC);

    // 处理 NULL 输入
    if (input == NULL) {
        VRT_fail(ctx, "my_module: input is NULL");
        return (NULL);
    }

    // 处理分配失败
    char *result = WS_Alloc(ctx->ws, size);
    if (result == NULL) {
        VRT_fail(ctx, "my_module: workspace overflow");
        return (NULL);
    }

    return (result);
}

9.4.3 线程安全

// VMOD 函数必须是线程安全的
// 避免使用全局变量
// 使用 PRIV_TASK 或 PRIV_TOP 存储请求级别的数据

struct priv_task {
    unsigned magic;
    #define PRIV_TASK_MAGIC 0x12345678
    char session_id[64];
};

VCL_STRING
vmod_get_session_id(VRT_CTX, struct vmod_priv *priv)
{
    struct priv_task *task;

    CHECK_OBJ_NOTNULL(ctx, VRT_CTX_MAGIC);

    if (priv->priv == NULL) {
        ALLOC_OBJ(task, PRIV_TASK_MAGIC);
        AN(task);
        // 生成 session ID
        snprintf(task->session_id, sizeof(task->session_id),
                 "sess-%ld-%d", time(NULL), rand());
        priv->priv = task;
        priv->free = free;
    } else {
        CAST_OBJ(task, priv->priv, PRIV_TASK_MAGIC);
    }

    return (task->session_id);
}

9.5 调试 VMOD

9.5.1 日志输出

#include "vsl.h"

VCL_STRING
vmod_debug_function(VRT_CTX, VCL_STRING input)
{
    // 使用 VSL 日志
    VSLb(ctx->vsl, SLT_Debug, "my_module: input=%s", input);

    // 使用 VRT_fail 记录错误
    if (input == NULL) {
        VRT_fail(ctx, "my_module: NULL input");
        return (NULL);
    }

    return (input);
}

9.5.2 单元测试

# tests/my_module.vtc

varnishtest "Test to_upper function"

server s1 {
    rxreq
    txresp -body "Hello World"
} -start

varnish v1 -vcl+backend {
    import my_module;

    sub vcl_recv {
        set req.http.X-Upper = my_module.to_upper("hello");
    }

    sub vcl_deliver {
        set resp.http.X-Upper = req.http.X-Upper;
    }
} -start

client c1 {
    txreq
    rxresp
    expect resp.http.X-Upper == "HELLO"
} -run

9.5.3 运行测试

# 运行所有测试
make check

# 运行单个测试
varnishtest tests/my_module.vtc

# 调试模式
varnishtest -v tests/my_module.vtc

9.6 常用 VMOD 示例

9.6.1 URL 解析 VMOD

$Module urlparse 1 "URL Parsing VMOD"

$Function STRING get_scheme(STRING url)
$Function STRING get_host(STRING url)
$Function STRING get_path(STRING url)
$Function STRING get_query(STRING url)
$Function STRING get_fragment(STRING url)
$Function STRING get_param(STRING query, STRING name, STRING default)
// src/vmod_urlparse.c
#include <string.h>
#include <stdlib.h>

VCL_STRING
vmod_get_path(VRT_CTX, VCL_STRING url)
{
    const char *p, *end;
    char *result;
    size_t len;

    if (url == NULL)
        return (NULL);

    // 跳过 scheme
    p = strstr(url, "://");
    if (p != NULL)
        p += 3;
    else
        p = url;

    // 跳过 host
    p = strchr(p, '/');
    if (p == NULL)
        return ("/");

    // 找到路径结束位置
    end = strchr(p, '?');
    if (end == NULL)
        end = strchr(p, '#');
    if (end == NULL)
        end = p + strlen(p);

    len = end - p;
    result = WS_Alloc(ctx->ws, len + 1);
    if (result == NULL)
        return (NULL);

    memcpy(result, p, len);
    result[len] = '\0';

    return (result);
}

9.6.2 JSON 操作 VMOD

$Module json 1 "JSON VMOD"

$Function STRING get_value(STRING json, STRING key)
$Function STRING get_string(STRING json, STRING path)
$Function INT get_int(STRING json, STRING path, INT default)
$Function BOOL get_bool(STRING json, STRING path, BOOL default)

9.7 注意事项

重要

  1. VMOD 使用 Varnish 工作空间(Workspace),空间有限,避免大量分配
  2. VMOD 函数必须是线程安全的,不能使用全局状态
  3. 使用 CHECK_OBJ_NOTNULL 验证对象有效性
  4. 错误时使用 VRT_fail 报告错误,而不是直接崩溃
  5. VMOD 版本号需要与 Varnish 版本兼容
  6. 编译 VMOD 需要与运行时相同的 Varnish 开发库版本

9.8 扩展阅读