Suricata原始碼分析-配置模組載入
阿新 • • 發佈:2022-04-21
初始化載入配置
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
-
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;
}
-
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; // 配置處理狀態賦值為:值
-
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;
}