1. 程式人生 > 其它 >Suricata原始碼分析-配置模組載入

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;
}