yzprofile's Notebook

All Posts| Note| Books| About
31 Aug 2012

ironbee

由于一直在做Web Server偏安全相关的开发,现在代码提交非常活跃的ironbee貌似值得看一下.

ironbee似乎是要励志于解决跨Web Server的安全引擎.不过现在只支持了apache的httpd和trafic server.

通过实现一个apache模块或者ATS模块作为ironbee engine的入口,将请求信息传递到ironbee内部.再经过ironbee内部各个模块处理这么一个流程.

项目主页:https://github.com/ironbee/ironbee

wiki里有安装使用的手册, 不过形同虚设…https://github.com/ironbee/ironbee/wiki/_pages

一个正在开发当中的项目,编译,使用过程中都会遇到各式各样的问题, 或许你遇到的问题和我不同…

Build Instructions: https://github.com/ironbee/ironbee/wiki/Build-instructions

假设你已经搞定了libhtp和libiconv动态库的问题,再或者意识到apache版本需要2.2.*才行..

”$ configure –with-httpd” 编译后会生成apache模块的相关支持动态库.

”$ make install” 会在 “/usr/local/ironbee/lib” 目录下生成mod_ironbee.so.

apache配置,加载ironbee:

LoadFile /usr/local/lib/libhtp.so
LoadModule ironbee_module /usr/local/ironbee/lib/mod_ironbee.so
<IfModule ironbee_module>
    LogLevel debug
    IronBeeEnable on
    IronBeeConfig /usr/local/ironbee/etc/ironbee-httpd.conf.example
</IfModule>

ironbee配置在:/usr/local/ironbee/etc/ironbee-httpd.conf.example,默认配置.

启动apache, curl一下.居然卡住了.之后等到超时返回”curl: (52) Empty reply from server”

来看下IronBeeConfig里的各个配置项,挨个关闭排查,发现是RequestBuffering引起的.把其off掉,apache就可以正常返回了.

ironbee手册:https://www.ironbee.com/docs/manual/ironbee-reference-manual.html

在手册里居然没有找到关于”RequestBuffering”的介绍.

那我们只好去源码里看看到底发生了什么,找到了下面这段代码:

    else if (strcasecmp("RequestBuffering", name) == 0) {
        ib_log_debug2(ib, "%s: %s", name, p1_unescaped);
        if (strcasecmp("On", p1_unescaped) == 0) {
            rc = ib_context_set_num(ctx, "buffer_req", 1);
            IB_FTRACE_RET_STATUS(rc);
        }

        rc = ib_context_set_num(ctx, "buffer_req", 0);
        IB_FTRACE_RET_STATUS(rc);
    }

很诡异的”ib_context_set_num”, 赋值时使用的变量居然是字符串, 我猜是用hash存的.

果不其然,又找到了如下代码:

ib_status_t ib_context_set_num(ib_context_t *ctx,
                               const char *name,
                               ib_num_t val)
{
    IB_FTRACE_INIT();
    ib_status_t rc = ib_cfgmap_set(ctx->cfg, name, ib_ftype_num_in(&val));
    IB_FTRACE_RET_STATUS(rc);
}

ib_status_t ib_cfgmap_set(ib_cfgmap_t *cm,
                          const char *name,
                          void *in_val)
{
    IB_FTRACE_INIT();
    ib_field_t *f;
    ib_status_t rc;

    rc = ib_hash_get(cm->hash, &f, name);
    if (rc != IB_OK) {
        IB_FTRACE_RET_STATUS(rc);
    }

    rc = ib_field_setv(f, in_val);

    IB_FTRACE_RET_STATUS(rc);
}

找到配置结构的定义:

static IB_CFGMAP_INIT_STRUCTURE(core_config_map) = {

    /* Logger */
    IB_CFGMAP_INIT_ENTRY(
        IB_PROVIDER_TYPE_LOGGER,
        IB_FTYPE_NULSTR,
        ib_core_cfg_t,
        log_handler
    ),
    IB_CFGMAP_INIT_ENTRY(
        IB_PROVIDER_TYPE_LOGGER ".log_level",
        IB_FTYPE_NUM,
        ib_core_cfg_t,
        log_level
    ),

...
...

    /* Buffering */
    IB_CFGMAP_INIT_ENTRY(
        "buffer_req",
        IB_FTYPE_NUM,
        ib_core_cfg_t,
        buffer_req
    ),

...
...
    /* End */
    IB_CFGMAP_INIT_LAST
};

不是很明白为什么又通过hash绕了一圈…从结构体中可以看到”.log_level”类似的指令.或许是为了支持这些.

继续跟”buffer_req”的行为,在其apache模块代码中:

    if (corecfg != NULL) {
        buffering = (int)corecfg->buffer_req;
    }

... 

        if (buffering) {
            ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, c->base_server,
                         "FETCH BRIGADE (buffering)");

            /* Normally Apache will request the headers line-by-line, but
             * IronBee does not require this.  So, here the request is
             * fetched with READBYTES and IronBee will then break
             * it back up into lines when it is injected back into
             * the brigade after the data is processed.
             */
            rc = ap_get_brigade(f->next,
                                bb,
                                AP_MODE_READBYTES,
                                block,
                                HUGE_STRING_LEN);
        }

...

还是先看下日志吧:

首先是httpd.conf:

把ironbee的日志等级改为debug
<IfModule ironbee_module>
    LogLevel debug
    IronBeeEnable on
    IronBeeConfig /usr/local/ironbee/etc/ironbee-httpd.conf.example
</IfModule>

apache的日志等级改为debug
ErrorLog "logs/error_log"
LogLevel debug

ironbee-httpd.conf.example:
### Logging
#Log /var/log/ironbee/debug.log
LogLevel trace
LogHandler mod_ironbee

三个日志.还真是有心…apache的日志,ironbee apache模块的日志, ironbee的日志

restart后再curl一下,日志卡在了”mod_ironbee.c(703): FETCH BRIGADE (buffering)”

我们添加两行日志:

            ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, c->base_server,
                         "FETCH BRIGADE (buffering-start)");
            rc = ap_get_brigade(f->next,
                                bb,
                                AP_MODE_READBYTES,
                                block,
                                HUGE_STRING_LEN);
            ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, c->base_server,
                         "FETCH BRIGADE (buffering-end)");

重新编译ironbee, make & make install.

再curl一下,看日志.为啥不一样了呢?尼玛…make install会用默认的配置再去覆盖你修改的.真贴心啊.

再把日志等级改成debug.

...

Sun Sep 02 21:58:47 2012] [debug] mod_ironbee.c(703): FETCH BRIGADE (buffering)
[Sun Sep 02 21:58:47 2012] [debug] mod_ironbee.c(712): FETCH BRIGADE (buffering-start)
[Sun Sep 02 21:58:47 2012] [debug] mod_ironbee.c(719): FETCH BRIGADE (buffering-end)

...

[Sun Sep 02 21:58:47 2012] [debug] mod_ironbee.c(703): FETCH BRIGADE (buffering)
[Sun Sep 02 21:58:47 2012] [debug] mod_ironbee.c(712): FETCH BRIGADE (buffering-start)

...

一次请求调用了两次”ap_get_brigade”, 第二次卡住了…

configure debug-enable再编译一次.再看看详细点…

重新修改配置.重启..curl.看日志..

呀,在trace级别的日志下,可以看到它把函数调用都打出来了,

    IB_FTRACE_INIT();
    IB_FTRACE_RET_STATUS(rc);

看下代码,发现是这两个宏的作用…它被写在每一个ironbee函数的入口和出口处,用来打印函数调用.可是为什么不用gdb编译时的选项来做这个事情呢(参见我之前的blog:step into source code)?把各种无用的代码夹杂在项目代码里.看起来挺丑的..

从日志上依然不能看出什么头绪,那只好拿出gdb了…

断点打在”ironbee_input_filter”,跟到”ap_get_brigade”,最终是卡在了”ap_core_input_filter”的”apr_bucket_read”上.能卡住的也只有”rv = apr_socket_recv(p, buf, len)”了.但是没想通为什么inputfillter又会去读socket呢?

也无力追究了…偏离主题了,我们还是把request_buf设置为off看下ironbee的表现吧.

先把日志级别从trace改回debug..打印函数输出太多了.

好吧..debug-enable编译的日志级别已经无效了…无论设置成什么都是trace…再重新编译一把…

更改默认配置,我们就按手册里说的那样配置一下,如果1大于0,在response header的阶段就删除响应头中的Server项

<Site default>
    SensorName ExampleSensorName
    SiteId AAAABBBB-1111-2222-3333-000000000000
    Hostname *
    RuleEnable all
    Rule 1 @gt 0 id:1 phase:RESPONSE_HEADER delResponseHeader:Server
</Site>

重新启动,配置加载成功,curl -i …

HTTP/1.1 200 OK
Date: Sun, 02 Sep 2012 19:58:25 GMT
Server: Apache/2.2.22 (Unix)
Last-Modified: Wed, 29 Aug 2012 09:42:09 GMT
ETag: "3864-6-4c8645f63df15"
Accept-Ranges: bytes
Content-Length: 6
Content-Type: text/html

似乎不起什么作用…

当然,我试了很多…或许是Sitename的问题?..试了很多组合查了手册…没能找到一个像样的完整的例子…另外去掉SiteId的配置居然还会coredump…

好吧…既然不能试试它的作用,那我们还是看下它的代码实现吧.

其在代码实现这层着实是可圈可点的

首先是ironbee的apache模块,在apache模块中

/**
 * Declare the module.
 */
module AP_MODULE_DECLARE_DATA ironbee_module = {
    STANDARD20_MODULE_STUFF,
    NULL,                       /* per-directory config creator */
    NULL,                       /* dir config merger */
    ironbee_create_config,      /* server config creator */
    ironbee_merge_config,       /* server config merger */
    ironbee_cmds,               /* command table */
    ironbee_register_hooks      /* set up other request processing hooks */
};

伴随着apache的启动,会初始化一个ironbee的engine环境.在apache执行的各个过程中挂载回调.

/**
 * Register functions to handle filters and hooks.
 */
static void ironbee_register_hooks(apr_pool_t *p)
{
    /* Other modules:
     *   mod_ssl       = AP_FTYPE_CONNECTION + 5
     *   mod_expires   = AP_FTYPE_CONTENT_SET - 2
     *   mod_cache     = AP_FTYPE_CONTENT_SET - 1
     *   mod_deflate   = AP_FTYPE_CONTENT_SET - 1
     *   mod_headers   = AP_FTYPE_CONTENT_SET
     */

    ap_register_input_filter(
        "IRONBEE_IN",
        ironbee_input_filter,
        NULL,
        AP_FTYPE_CONNECTION + 1
    );

#ifdef IB_DEBUG
    ap_register_input_filter(
        "IRONBEE_DBG_IN",
        ironbee_dbg_input_filter,
        NULL,
        AP_FTYPE_CONNECTION
    );
#endif

    ap_register_output_filter(
        "IRONBEE_OUT",
        ironbee_output_filter,
        NULL,
        AP_FTYPE_CONNECTION
    );

    ap_hook_child_init(ironbee_child_init,
                       NULL, NULL,
                       APR_HOOK_FIRST);

    ap_hook_post_config(ironbee_post_config,
                        NULL, NULL,
                        APR_HOOK_MIDDLE);

    ap_hook_pre_connection(ironbee_pre_connection,
                           NULL, NULL,
                           APR_HOOK_LAST);
}

通过”ironbee_post_config”在apache配置解析完毕后加载自己的配置进行解析.这时就进入了ironbee engine自己的地盘了.请求来时再通过ironbee的inputfillter和outputfilter进行过滤.

ironbee的apache模块mod_ironbee就像一个桥梁,连接了ironbee engine和apache.将请求信息带给ironbee进行分析处理.从而实现了ironbee跨web server的第一步.分析引擎统一,只用去实现各个web server的接口模块就可以了.

/**
 * Additional functionality for IronBee.
 *
 * A module provides additional functionality to IronBee.  It can register
 * configuration values and directives, hook into events, and provide
 * functions for other modules.
 */
struct ib_module_t {
    /* Header */
    uint32_t     vernum;           /**< Engine version number */
    uint32_t     abinum;           /**< Engine ABI Number */
    const char  *version;          /**< Engine version string */
    const char  *filename;         /**< Module code filename */
    void        *data;             /**< Module data */
    ib_engine_t *ib;               /**< Engine */
    size_t       idx;              /**< Module index */


    /* Module Config */
    const char *name; /**< Module name */

    void                    *gcdata;          /**< Global config data */
    size_t                   gclen;           /**< Global config data length */
    ib_module_fn_cfg_copy_t  fn_cfg_copy;     /**< Config copy handler */
    void                    *cbdata_cfg_copy; /**< Config copy data */
    const ib_cfgmap_init_t  *cm_init;         /**< Module config mapping */

    const ib_dirmap_init_t *dm_init; /**< Module directive mapping */

    /* Functions */
    ib_module_fn_init_t         fn_init;         /**< Module init */
    void                       *cbdata_init;     /**< fn_init callback data */
    ib_module_fn_fini_t         fn_fini;         /**< Module finish */
    void                       *cbdata_fini;     /**< fn_init callback data */
    ib_module_fn_ctx_open_t     fn_ctx_open;     /**< Context open */
    void                       *cbdata_ctx_open; /**< fn_init callback data */
    ib_module_fn_ctx_close_t    fn_ctx_close;    /**< Context close */
    void                       *cbdata_ctx_close;/**< fn_init callback data */
    ib_module_fn_ctx_destroy_t  fn_ctx_destroy;  /**< Context destroy */
    void                       *cbdata_ctx_destroy; /**< fn_init callback data */
};

看到这个结构体,熟悉Nginx模块开发的同学应该已经知道要发生什么了.

以geoip模块为例:

/* Initialize the module structure. */
IB_MODULE_INIT(
    IB_MODULE_HEADER_DEFAULTS,           /* Default metadata */
    MODULE_NAME_STR,                     /* Module name */
    IB_MODULE_CONFIG_NULL,               /* Global config data */
    NULL,                                /* Configuration field map */
    geoip_directive_map,                 /* Config directive map */
    geoip_init,                          /* Initialize function */
    NULL,                                /* Callback data */
    geoip_fini,                          /* Finish function */
    NULL,                                /* Callback data */
    NULL,                                /* Context open function */
    NULL,                                /* Callback data */
    NULL,                                /* Context close function */
    NULL,                                /* Callback data */
    NULL,                                /* Context destroy function */
    NULL                                 /* Callback data */
);

/* 指令 */
static IB_DIRMAP_INIT_STRUCTURE(geoip_directive_map) = {

    /* Give the config parser a callback for the directive GeoIPDatabaseFile */
    IB_DIRMAP_INIT_PARAM1(
        "GeoIPDatabaseFile",
        geoip_database_file_dir_param1,
        NULL
    ),

    /* signal the end of the list */
    IB_DIRMAP_INIT_LAST
};

/* 相当于Nginx中这个 */
static ngx_command_t  ngx_http_geoip_commands[] = {

      ngx_null_command
};

指令解析时打开文件,load配置

在初始化时挂载回调

/* Called when module is loaded. */
static ib_status_t geoip_init(ib_engine_t *ib, ib_module_t *m, void *cbdata)
{
    IB_FTRACE_INIT();

    ib_status_t rc;

    if (geoip_db == NULL)
    {
        ib_log_debug(ib, "Initializing default GeoIP database...");
        geoip_db = GeoIP_new(GEOIP_MMAP_CACHE);
    }

    if (geoip_db == NULL)
    {
        ib_log_debug(ib, "Failed to initialize GeoIP database.");
        IB_FTRACE_RET_STATUS(IB_EUNKNOWN);
    }

    ib_log_debug(ib, "Initializing GeoIP database complete.");

    ib_log_debug(ib, "Registering handler...");

    rc = ib_hook_tx_register(ib,
                             handle_context_tx_event,
                             geoip_lookup,
                             NULL);

    ib_log_debug(ib, "Done registering handler.");

    if (rc != IB_OK)
    {
        ib_log_debug(ib, "Failed to load GeoIP module.");
        IB_FTRACE_RET_STATUS(rc);
    }

    ib_log_debug(ib, "GeoIP module loaded.");
    IB_FTRACE_RET_STATUS(IB_OK);
}

这句是值得关注的.在nginx中的phase handler在ironbee对应起来就是event.register和apache的register也挺像,接下来的你就懂了….

    rc = ib_hook_tx_register(ib,
                             handle_context_tx_event,
                             geoip_lookup,
                             NULL);

geoip_lookup会在请求来时查询ip库,赋值给ironbee的变量…

似乎和nginx是一个套路的…

让人比较郁闷的是ironbee大量使用了宏,甚至在一些不需要的地方也大肆使用.导致代码跟踪简直一塌糊涂.

已经在它上面花费了不少时间了,不想再纠缠下去了.今天就到这里吧.

对比其和mod security,其高度的模块化和跨web server是其最大的亮点.可是写了那么长的文档,没有给出一个完整的能跑的例子真是点点点….

什么是真正的吐槽无力?就是在你不能正常使用的情况下你不能确定是丫写的太高端还是我自己太笨…

正在开发中的项目伤不起…

have fun :)

EOF