Suricata源码分析-配置模块加载

初始化加载配置

ConfYamlLoadFile(主函数)

模块主函数是通过SuricataMain -> LoadYamlConfig -> ConfYamlLoadFile 进入的,Suricata中解析YAML文件是通过c语言libyaml库实现的,libyaml库有三种方式来解析YAML文件,本程序中使用了基于事件(event-based)的方式。

tips:以下说明默认了解libyaml库的基本使用

/* 从YAML文件加载配置。
 * 这个函数将加载一个配置文件。失败返回-1并退出,加载配置文件时的任何错误都将被记录下来
 * 参数:要加载的配置文件名字
 */
int ConfYamlLoadFile(const char *filename)
{
    FILE *infile;         // 文件指针
    yaml_parser_t parser; // yaml解析器
    int ret;
    ConfNode *root = ConfGetRootNode(); // 配置节点树的根节点

    // 初始化yaml解析器
    if (yaml_parser_initialize(&parser) != 1) {
        SCLogError(SC_ERR_FATAL, "failed to initialize yaml parser.");
        return -1;
    }

    // 检查文件名,是否是目录
    struct stat stat_buf;
    if (stat(filename, &stat_buf) == 0) {
        if (stat_buf.st_mode & S_IFDIR) {
            SCLogError(SC_ERR_FATAL,
                    "yaml argument is not a file but a directory: %s. "
                    "Please specify the yaml file in your -c option.",
                    filename);
            yaml_parser_delete(&parser);
            return -1;
        }
    }

    // 打开文件并检查文件指针
    // coverity[toctou : FALSE]
    infile = fopen(filename, "r");
    if (infile == NULL) {
        SCLogError(SC_ERR_FATAL, "failed to open file: %s: %s", filename, strerror(errno));
        yaml_parser_delete(&parser);
        return -1;
    }

    // 设置配置文件的目录名
    if (conf_dirname == NULL) {
        ConfYamlSetConfDirname(filename);
    }

    // 打开YAML文件,开始递归解析
    yaml_parser_set_input_file(&parser, infile);
    ret = ConfYamlParse(&parser, root, 0, 0);
    yaml_parser_delete(&parser);
    fclose(infile);

    return ret;
}

ConfYamlParse(递归解析函数)

ConfYamlParse是一个递归的函数,按层级解析yaml文件(当递归次数达到128时,解析会失败)。这个函数主要是按照事件类型(详见yaml.h)判断,进行对应的操作。以下截取几个重要的事件类型进行说明

1. Document Start事件

suricata.yaml文件最开始写有字符串%YAML 1.1 ---,是用来注明YAML文件版本的。当该事件触发时,程序会验证YAML文件版本,来判断此配置文件是否有效。

// 验证YAML版本,如果版本正确,它就更有可能是一个有效的Suricata配置文件
yaml_version_directive_t *ver = event.data.document_start.version_directive;
// 版本信息为空,输出提示信息跳转失败
if (ver == NULL) {
    SCLogError(SC_ERR_CONF_YAML_ERROR, "ERROR: Invalid configuration file.");
    SCLogError(SC_ERR_CONF_YAML_ERROR, "The configuration file must begin with the "
                                       "following two lines: %%YAML 1.1 and ---");
    goto fail;
}
// 验证版本信息
int major = ver->major;
int minor = ver->minor;
// 验证失败,输出提示信息跳转失败
if (!(major == YAML_VERSION_MAJOR && minor == YAML_VERSION_MINOR)) {
    SCLogError(SC_ERR_CONF_YAML_ERROR, "ERROR: Invalid YAML version.  Must be 1.1");
    goto fail;
}

2. Scalar事件

scalar是最主要的事件,用来获取具体的配置内容。

首先根据参数inseq判断是否是列表,如果是列表就开始添加节点

// 拼接节点名
char sequence_node_name[DEFAULT_NAME_LEN];
snprintf(sequence_node_name, DEFAULT_NAME_LEN, "%d", seq_idx++);
// 通过名称查找子节点
ConfNode *seq_node = ConfNodeLookupChild(parent, sequence_node_name);
// 如果节点不为空,就删除它
if (seq_node != NULL) {
    /* 这个序列节点已经被设置,可能是从命令行中。删除它,
     * 以便按照迭代的期望序列重新添加。
     */
    TAILQ_REMOVE(&parent->head, seq_node, next);
} else { // 如果节点为空,则分配一个新节点
    seq_node = ConfNodeNew();
    if (unlikely(seq_node == NULL)) {
        goto fail;
    }
    // 拷贝节点名称
    seq_node->name = SCStrdup(sequence_node_name);
    if (unlikely(seq_node->name == NULL)) {
        SCFree(seq_node);
        goto fail;
    }
    // 如果节点value不为空,则拷贝
    if (value != NULL) {
        seq_node->val = SCStrdup(value);
        if (unlikely(seq_node->val == NULL)) {
            SCFree(seq_node->name);
            goto fail;
        }
    } else { // 节点值为空
        seq_node->val = NULL;
    }
}
// 插入节点到队伍末尾
TAILQ_INSERT_TAIL(&parent->head, seq_node, next);

如果不是列表,再根据配置处理状态分为三种情况:CONF_INCLUDE,CONF_KEY,CONF_VALUE

  1. CONF_INCLUDE:include关键字用来包含另一个YAML文件,要调用ConfYamlHandleInclude函数解析另外的YAML文件
// 用来解析配置中包含的YAML文件
static int ConfYamlHandleInclude(ConfNode *parent, const char *filename)
{
    yaml_parser_t parser;
    char include_filename[PATH_MAX];
    FILE *file = NULL;
    int ret = -1;

    // 初始化yaml对象
    if (yaml_parser_initialize(&parser) != 1) {
        SCLogError(SC_ERR_CONF_YAML_ERROR, "Failed to initialize YAML parser");
        return -1;
    }

    // 验证路径是否是绝对路径
    if (PathIsAbsolute(filename)) {
        strlcpy(include_filename, filename, sizeof(include_filename));
    } else {
        // 拼接出绝对路径
        snprintf(include_filename, sizeof(include_filename), "%s/%s", conf_dirname, filename);
    }

    // 打开文件并检测
    file = fopen(include_filename, "r");
    if (file == NULL) {
        SCLogError(SC_ERR_FOPEN, "Failed to open configuration include file %s: %s",
                include_filename, strerror(errno));
        goto done;
    }

    // 关联文件句柄和yaml对象
    yaml_parser_set_input_file(&parser, file);

    // 递归解析
    if (ConfYamlParse(&parser, parent, 0, 0) != 0) {
        SCLogError(SC_ERR_CONF_YAML_ERROR, "Failed to include configuration file %s", filename);
        goto done;
    }

    ret = 0;

    // 清理环境,退出
done:
    yaml_parser_delete(&parser);
    if (file != NULL) {
        fclose(file);
    }

    return ret;
}
  1. CONF_KEY:获取到键,添加新节点,检测是否有非法字符'_'并输出警告
// include关键字用来包含另一个yaml文件,需要另外解析
if (strcmp(value, "include") == 0) {
    state = CONF_INCLUDE;
    goto next;
}

/* 判断父节点如果是序列,并且为空。则将
* value 拷贝过去,并替换不支持的字符
*/
if (parent->is_seq) {
    if (parent->val == NULL) {
        parent->val = SCStrdup(value);
        if (parent->val && strchr(parent->val, '_'))
            // 损坏不支持的字符
            Mangle(parent->val);
    }
}
// 通过名称查找子节点
ConfNode *existing = ConfNodeLookupChild(parent, value);
// 如果查找结果不为空,需要重定义节点
if (existing != NULL) {
    if (!existing->final) {
        SCLogInfo("Configuration node '%s' redefined.", existing->name);
        // 修整配置节点(类似释放,但留下final参数)
        ConfNodePrune(existing);
    }
    node = existing;
} else {    
    // 查找结果为空,则分配一个新的节点
    node = ConfNodeNew();
    // 拷贝数据
    node->name = SCStrdup(value);
    // 遇到不支持的字符'_'且不属于特殊的两组的配置,则输出警告:不建议使用……
    if (node->name && strchr(node->name, '_')) {
        if (!(parent->name &&
                    ((strcmp(parent->name, "address-groups") == 0) ||
                            (strcmp(parent->name, "port-groups") == 0)))) {
            // 损坏不支持的字符
            Mangle(node->name);
            if (mangle_errors < MANGLE_ERRORS_MAX) {
                SCLogWarning(SC_WARN_DEPRECATED,
                        "%s is deprecated. Please use %s on line %" PRIuMAX ".",
                        value, node->name, (uintmax_t)parser->mark.line + 1);
                mangle_errors++;
                // 警告提示10次以上则不再提示
                if (mangle_errors >= MANGLE_ERRORS_MAX)
                    SCLogWarning(SC_WARN_DEPRECATED,
                            "not showing more "
                            "parameter name warnings.");
            }
        }
    }
    // 插入节点到队伍末尾
    TAILQ_INSERT_TAIL(&parent->head, node, next);
}
state = CONF_VAL;   // 配置处理状态赋值为:值
  1. CONF_VALUE:获取到值,添加到对应的键下
if (value != NULL && (tag != NULL) && (strcmp(tag, "!include") == 0)) {
    SCLogInfo("Including configuration file %s at "
              "parent node %s.",
            value, node->name);
    // 解析配置中包含的YAML文件
    if (ConfYamlHandleInclude(node, value) != 0)
        goto fail;
} else if (!node->final && value != NULL) {
    // 拷贝数据
    if (node->val != NULL)
        SCFree(node->val);
    node->val = SCStrdup(value);
}
state = CONF_KEY;

3. Sequence Start事件

sequence事件说明此时是列表,需要继续递归解析下一层信息

4. Mapping Start事件

mapping事件触发时,根据参数inseq决定是否开始添加节点,或是继续下一层

SCLogDebug("event.type=YAML_MAPPING_START_EVENT; state=%d", state);
if (inseq) { // 如果是序列,开始添加节点
    char sequence_node_name[DEFAULT_NAME_LEN];
    snprintf(sequence_node_name, DEFAULT_NAME_LEN, "%d", seq_idx++);
    // 根据名称查找子配置节点
    ConfNode *seq_node = ConfNodeLookupChild(node, sequence_node_name);
    if (seq_node != NULL) {
        // 序列节点已经被设置,可能是从命令行。删除它,以便按照迭代的预期序列重新添加
        TAILQ_REMOVE(&node->head, seq_node, next);
    } else {
        // 分配一个新的配置节点
        seq_node = ConfNodeNew();
        // 检查结果
        if (unlikely(seq_node == NULL)) {
            goto fail;
        }
        // 拷贝节点名称
        seq_node->name = SCStrdup(sequence_node_name);
        if (unlikely(seq_node->name == NULL)) {
            SCFree(seq_node);
            goto fail;
        }
    }
    seq_node->is_seq = 1;
    // 插入到队列末尾
    TAILQ_INSERT_TAIL(&node->head, seq_node, next);
    // 继续递归
    if (ConfYamlParse(parser, seq_node, 0, rlevel) != 0)
        goto fail;
} else { // 如果不是序列,继续递归解析
    if (ConfYamlParse(parser, node, inseq, rlevel) != 0)
        goto fail;
}
state = CONF_KEY; // 配置处理状态:键

重载配置

配置的重载是在SuricataMainLoop主线程循环中,当程序接收到信号,会调用DetectEngineReload函数重新加载规则、配置等相关信息。关于配置的重载主要是通过ConfYamlLoadFileWithPrefix函数实现

/* 重载时调用的加载配置函数:从YAML文件中加载配置,插入到树的‘prefix’处
* 
* 这个函数将加载一个配置文件并插入到配置树的‘prefix’节点。这意味着,如果以前缀“abc”
* 调用这个函数,并且文件包含参数“def”,他将作为“abc.def”被加载。
* 
* 参数1:文件名
* 参数2:要使用的前缀名
* 返回值:成功返回0,失败返回-1
*/
int ConfYamlLoadFileWithPrefix(const char *filename, const char *prefix)
{
    FILE *infile;
    yaml_parser_t parser;
    int ret;
    ConfNode *root = ConfGetNode(prefix);

    // 初始化yaml对象
    if (yaml_parser_initialize(&parser) != 1) {
        SCLogError(SC_ERR_FATAL, "failed to initialize yaml parser.");
        return -1;
    }

    // 检查 配置文件是否是目录
    struct stat stat_buf;
    /* coverity[toctou] */
    if (stat(filename, &stat_buf) == 0) {
        if (stat_buf.st_mode & S_IFDIR) {
            SCLogError(SC_ERR_FATAL,
                    "yaml argument is not a file but a directory: %s. "
                    "Please specify the yaml file in your -c option.",
                    filename);
            return -1;
        }
    }

    // 打开配置文件
    /* coverity[toctou] */
    infile = fopen(filename, "r");
    if (infile == NULL) {
        SCLogError(SC_ERR_FATAL, "failed to open file: %s: %s", filename, strerror(errno));
        yaml_parser_delete(&parser);
        return -1;
    }

    // 设置配置文件的目录名
    if (conf_dirname == NULL) {
        ConfYamlSetConfDirname(filename);
    }

    // 如果作为前缀的节点不存在,添加一个占位符
    if (root == NULL) {
        ConfSet(prefix, "<prefix root node>");
        // 通过名字查找节点
        root = ConfGetNode(prefix);
        // 如果不存在,退出函数
        if (root == NULL) {
            fclose(infile);
            yaml_parser_delete(&parser);
            return -1;
        }
    }
    // 开始递归解析
    yaml_parser_set_input_file(&parser, infile);
    ret = ConfYamlParse(&parser, root, 0, 0);
    yaml_parser_delete(&parser);
    fclose(infile);

    return ret;
}
posted @ 2022-04-21 16:07  6c696e  阅读(166)  评论(0)    收藏  举报