From 2d6ffdd166fb63dadd053fa99dad415cc0d9401a Mon Sep 17 00:00:00 2001 From: liuwentan Date: Wed, 5 Jul 2023 21:47:58 +0800 Subject: [PATCH] first draft --- docs/imgs/thread_mode.png | Bin 0 -> 45185 bytes docs/introduction.md | 27 +- docs/table_data.md | 253 ++++++++-------- docs/table_schema.md | 592 +++++++++++++++++++++++++++++++++++--- docs/thread_mode.md | 66 +++++ readme.md | 2 + 6 files changed, 772 insertions(+), 168 deletions(-) create mode 100644 docs/imgs/thread_mode.png create mode 100644 docs/thread_mode.md diff --git a/docs/imgs/thread_mode.png b/docs/imgs/thread_mode.png new file mode 100644 index 0000000000000000000000000000000000000000..1a12494c181502c5f02baca6bc6d58cd32af7328 GIT binary patch literal 45185 zcmdRWV|ZQ77jM)i4Vt7$8rwXegEovzuqr5=Q+>W zdkxmi(yaOI*$I`C5k-K-f&~KuLl74eQUC*kcm)FkcY}EkqU^EJ34w0J%>)JI#03Qj zljGukV-$n^5lOsS-?jSJl4_;lz$VMpmE3?nbL5G(lw>LtEWymlm}ClFt=ljd4=pb z`1MYh6dpc;lCRO;O&mOA1>7(k6&&}S;TU+$6yY%ff%B&TFQ{@dk^#(t4sb&_5)%@L zkuRR$hDRiXp+&?sIk)~`HfjN}-vj+BW1A|2;lS_W;3X8OCHU$cy!nmfjon2;{fX%l z6wtV?;%*#KBj6(=AY-1frl@@dwqK=@9XN~N%h+}3wV3;0X)9F zo#|&kxL{y>+y)1uq55JQ8nuxHlC6r7nz*sFG#C}=8U_qJ*bEE`bOjFjVu8M3V32V^ zV9=o7PoS?*HpIV6Azrf~|9uVa_PZj#qM*1q=vUFe-pI(>!PLgFepg}x)YYt+lA5EM zv=oPdjTN1qp^d%~ovW4Y?tVJ zi0C(?|NQ+uP9s;d|8lZ+_}8>R69oJ&0Wi?f1OC%Ds43TPDujwFIq`py16&OQRx<3J+KGj{TZI zlMG4!QHBWs{pC2_Sj9P;jlH)my{iVLKvc&)A#}V(%Nc&)7v=5_RFpJoT`l`*cMN!NrpzF+#ZVem&~ zvM+ofGPYca7U@6elD@b;nna$%|I;(52D})E3`vz(*7GOb@VJmuyE|X- z-(MdIAs`p;)Q%)1UNe(ypjZT4WyMzbE*2v!+Mct}kH+7e20Ddj zb3CAyB}c3R>~%D%YXbrU*%SCqGhCATHhNi*F)=YGF_|y*zgw=iuo>#mfvF2KQBz~U zb}$0o#gxbG$0p+$5gF1XCvH&O54NH5wWJ5AQA(0CjMaw8lY}e!%6nK|FkDZDG*m&>R@4mKXv>6$>0eHNJ$IE zV?Ev6+!PXe_3aXbE*^%5r6{djU7I&8a7Ycm7UWm)r$6BuaEQm#s&xD%&|Uy-qndb z$(NCqP8{v)?Oh0UIzt`mZL|DPY@}v^2OGkc?(TW=$HJbx;3`RBn)( z$c!+zmYQrKGD|tVS&*bFSgSs?UE9Cb!uXu=r`gbhzx{SkpTo$4f4HqR$o|%7wbE1m zY;aEwGMpDh8=8L@tZg_L=d1O`X3wAY*ToA8EoWKo1||OpIhr7Ya)Co9{|DDsagag1 zO=C-r{Ap;*M4(Y>HnaUB6rIb0!s1iWs+xH`N;bN+iJ>6@FYg*%=<%`TIDYUX6f7(( ziS@O(AA90%x9DFZQ-!j(w+~x`viLUJ7}*fHw{=PWh0+IE4T3Bi5h67;Rjt{U@b-8C z(p}j(Pb6}@OchXXIw61$p&=fdkPr}s%c=i(eb9W`a#XG#*1>b$1qt)v!;d*#A8oSH z6%?Ph7uH701%Etl_iX8JEJk#^E~hxV!>M1>m@PI(InQWNs%AnnI9)cgd|rRNzC0o0 z;nDqaemZF)W@DSzi&cvLX48R5ty(E`wL4$rb-x;^R66%HTNYPDKoxjvdhq*f_Ee_Rzu z#K`ix#r*m6=lOXzVlrj842^CFPB0>VpD#G1T$u`OtJ%Gw8N~9IZW=UxYC_j+g{ECM z0_*z2URHmL(-Ge&4f_m}@$h&6412jjf7A?#O#{=*)fg{>_p@v8EM|+OIbnQm zcT`#K8&o>&mTmf~wl=N^Os4m&tFFX_G8ti0MY0BWrz@_f-}h`UHid)d?4W+fHoj=6 z-LWiyQ@I?c`~Em#I+xog6f&u<$D1Q%^kfnCHTNT0%jIVIo3@$J7(-Grn~n#v*-C9q zGLOS@kBva|d|-bRE;Y*8X(&kga~ocxD{a^5nI6wK<+w>BorO9R=2*Rh$%0A9KI*ErT=7?P6D%YoFmu!r)>8F4;**<(a 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�Ld_dERnC~b+W z_!P`eW4-_{u=-l(9!Dc}6)*`=9y`7}Jxnhp(frU?p)H-wz_|JP{9mwz%eJ+(v8KP0)n-{5FK< zHGwcXgCcAQqKOFqwB^#Z<{+l-<%%&LnqC)U^wk-I1D^c2?G9I2SVIHd6IoMWhS6rx zTdn1&nxLDxJ(KtVz4*e!G8NdA{^e=Tbo{q*#|~ro&ky)IE&k+w)RTAAAducJ(Y}2N z?f;3f>p&CNGPVAH^j>LFrRy_hE!!aU?mg8z6!aPx{)mZls~h-hJE$NU_vC`oGwC=H z_Uq*_3BNg`GaWD#bCu_0Kdu`^|C{m;SPiB}oSM~4H_4{I&2yRtj-)QSw^P01vH)z@ zIPU)zNh~S7cf#lT5G{k8ekuUIA6fRQiI^&_;|bM?8vUx_Xz zpc$mB!#fHqQorXKm*b2P?4mN;o**^+=YvE&X)yy?7#TsoOY=X=66i#z{X-Vu*-!o? zG1QYePLQ0>Wa5thls`~U0>VIt5O7{rzyDY7`FRKQo^#qBpTFB{2koVPaACgt*GV5m zfb8raYbo|ud*+Wpdn(34#F4*8`el{yR0imYW_cc}zuM#C2JJDHQH1}y_;q>B;ArQx z;Ihr=QwaWQFF6^sN2G^;>CgD@%Kd@%+~SPu3;C-(W#!9#CpLxSw}4nb(FFe3DkD)) z69gF$wWj>QuX8#Q)=Ng#h3M(!z9VPT11BUwAi8^n$mR68C12%_lz zG?)hoeOUX-0+%s8zyuz!shahkIx$AhNuTxd*Owhi0B$wwWlR;HWG7am*8@)sM*8l! n40a#j`)}_gnO>Q;w>t5U_X*%QeQA1#27VM|RHe%#&0qW< Redis mode + +Maat可以通过Redis的主从同步机制,实现配置的分发。本节介绍MAAT加载Redis中配置时,对存储结构的要求。和数据库一样,Redis存储结构的设计上不需要考虑编译、分组和域的逻辑层次。由配置更新线程,通过行列式配置重构各层次间的组合关系。 +![Sync Rule with Redis](./imgs/data-sync-with-redis.png) + +### 1.1 Transactional Write +表 26 MAAT Redis中定义的数据结构 + +| Redis KEY | 名称 | 结构 | 用途 | +| ------------------------------------------------------ | ---------------- | -------------- | ------------------------- | +| MAAT_VERSION | primary version | INTERGER | 标识Redis中配置的版本号。当redis中版本号大于MAAT中配置版本号时,会去读取MAAT_UPDATE_STATUS。 | +| MAAT_PRE_VERSION | 预备版本 | INTERGER | | +| MAAT_TRANSACTION_xx | 事务配置状态 | LIST | 用于临时存储事务中的配置状态,xx为MAAT_PRE_VERSION其中的状态在事务结束后会被更新到MAAT_UPDATE_STATUS,本身被删除。 | +| MAAT_UPDATE_STATUS | 配置状态 | sorted set, member是配置规则,score 为版本号,详见11.3 | MAAT会用ZRANGEBYSCORE命令读取。 | +| MAAT_RULE_TIMER | 主配置超时信息 | sorted set, member是配置规则,score为超时间,详见11.4 | MAAT配置更新线程会定时检查超时状况,并设置超时状态。 | +| MAAT_VERSION_TIMER | 版本创建时间 | sorted Set | 存储了每个版本的创建时间,score为版本创建时间,member 为version,用以将MAAT_UPDATE_STATUS维持在一个较小的规模。 | +| MAAT_LABEL_INDEX | 标签索引 | sorted set, element 是配置表名,编译配置ID,score为label_id | | +| EFFECTIVE_RULE:TableName,ID OBSOLETE_RULE:TableName,ID | 主配置 |string | 生效中的配置,结构与10.3中的行结构相同,MAAT会逐条加载。 | +| SEQUENCE_ REGION | 域ID生成序列号 | INTERGER | 用于生产者生成不重复的region_id | +| SEQUENCE_ GROUP | 分组ID生成序列号 | INTERGER | 用于生产者生成不重复的group_id | +| EXPIRE_OP_LOCK | 分布式锁 | 字符串”locked" | 用于保证最多只有一个写者进行淘汰。 | + + +Maat command API 可直接将配置写入 redis +```c +struct maat_cmd_line { + const char *table_name; + const char *table_line; + long long rule_id; // for MAAT_OP_DEL, only rule_id and table_name are necessary. + int expire_after; //expired after $timeout$ seconds, set to 0 for never timeout. +}; + +int maat_cmd_set_line(struct maat *maat_inst, const struct maat_cmd_line *line_rule); + +Example: + char table_line[1024] = {0}; + long long item_id = 100; + long long group_id = 200; + const char *keywords = "Hello&Maat"; + int expr_type = 1; //EXPR_TYPE_AND + int match_method = 0; //MATCH_METHOD_SUB + int is_hexbin = 0; + int op = 1; //add + + sprintf(table_line, "%lld\t%lld\t%s\t%d\t%d\t%d\t%d", item_id, group_id, + keywords, expr_type, match_method, is_hexbin, op); + + struct maat_cmd_line line_rule; + line_rule.rule_id = item_id; + line_rule.table_line = table_line; + line_rule.table_name = table_name; + line_rule.expire_after = expire_after; + + int ret = maat_cmd_set_line(maat_inst, &line_rule); +``` + +### 1.2 主版本号、预备版本号与Lua Script + +生产者写入配置时,先对预备版本号加1,并作为写入配置状态的score,待写入完成后,再对主版本号加1。放弃WATCH MAAT_VERSION的事务。这一方法可以大幅提高写入性能,除ID冲突外,可确保写入成功。 + +当有多个生产者时,可能存在配置状态与主版本号不一致的问题。主版本号为v,某次更新时在配置状态中声明的版本号为u,消费者增量更新时有以下情况: + +- 若v=u,则版本号一致,配置正常加载; +- 若v>u,该情况不存在。因为只有配置状态修改完成,主版本号才会增加1。换句话说,每次写入都是先增加预备版本号,后增加主版本号,所以主版本号必然小于或等于配置状态中的最大版本号。 + - 错误:三个生产者情况下,有问题,如下表。 +- 若vmv,本次更新的增量将在下一次 +- tvMaat版本+1,则说明有遗漏的更新(网络长时间中断),启用全量更新流程。 + +对于DEL状态,如果查询不到对应的主配置状态,同样说明有遗漏更新(网络中断时间超过OBSOLETE_RULE超时时间),启用全量更新流程。 + +#### MAAT_EXPIRE_TIMER + +该结构使用Sorted Set存储了主配置的超时信息,score为绝对超时时间,member的结构为TableName,ID。 + + +#### MAAT_VERSION_TIMER + +该结构使用Sorted Set存储了每个版本的创建时间,score为版本创建时间,member为 版本号(version),即MAAT_UPDATE_STATUS的score,用以将MAAT_UPDATE_STATUS维持在一个较小的规模。 + +#### 主配置结构 + +有两类配置命名方式: + +1. EFFECTIVE_RULE:TableName,ID 表示正在生效的配置; +2. OBSOLETE_RULE:TableName,ID 表示已经删除的配置,这些配置超时(EXPIRE)后会被Redis删除。 + +### Load From Redis +Maat实例的工作线程定时轮询Redis中MAAT_VERSION,如果大于实例的MAAT_VERSION则进行更新。 + +![Load from Redis](./imgs/load-data-from-redis.png) + +### 读写性能 + +为保证事务,Redis需工作在单机+主从模式。带超时的配置写入5000条/秒,无超时配置10000条/秒。 + + + +## 2. Iris mode 在Maat可以监听全量和增量目录下的文件来更新配置运行时变化,下面对这种模式下的文件格式进行介绍。 @@ -60,106 +194,7 @@ 2. 在配置汇总表中增加该配置的汇总信息,注意要和库表文件中的compile_id一致,且不能与已有compile_id冲突,修改文件第一行的行数 3. 在库表索引文件中修改配置汇总表和域表的行数 -## Redis配置加载接口 - -Maat可以通过Redis的主从同步机制,实现配置的分发。本节介绍MAAT加载Redis中配置时,对存储结构的要求。和数据库一样,Redis存储结构的设计上不需要考虑编译、分组和域的逻辑层次。由配置更新线程,通过行列式配置重构各层次间的组合关系。 -![Sync Rule with Redis](./imgs/data-sync-with-redis.png) - -### Transactional Write -表 26 MAAT Redis中定义的数据结构 - -| Redis KEY | 名称 | 结构 | 用途 | -| ------------------------------------------------------- | ---------------- | ----------------------------------------------------------- | ------------------------------------------------------------ | -| MAAT_VERSION | 主版本号 | INTERGER | 标识Redis中配置的版本号。当redis中版本号大于MAAT中配置版本号时,会去读取MAAT_UPDATE_STATUS。 | -| MAAT_PRE_VERSION | 预备版本号 | INTERGER | | -| MAAT_TRANSACTION_xx | 事务配置状态 | LIST | 用于临时存储事务中的配置状态,xx为MAAT_PRE_VERSION其中的状态在事务结束后会被更新到MAAT_UPDATE_STATUS,本身被删除。 | -| MAAT_UPDATE_STATUS | 配置状态 | sorted set, member是配置规则,score 为版本号,详见11.3 | MAAT会用ZRANGEBYSCORE命令读取。 | -| MAAT_RULE_TIMER | 主配置超时信息 | sorted set, member是配置规则,score为超时间,详见11.4 | MAAT配置更新线程会定时检查超时状况,并设置超时状态。 | -| MAAT_VERSION_TIMER | 版本创建时间 | sorted Set | 存储了每个版本的创建时间,score为版本创建时间,member 为version,用以将MAAT_UPDATE_STATUS维持在一个较小的规模。 | -| MAAT_LABEL_INDEX | 标签索引 | sorted set, element 是配置表名,编译配置ID,score为label_id | | -| EFFECTIVE_RULE:TableName,ID OBSOLETE_RULE:TableName,ID | 主配置 | string | 生效中的配置,结构与10.3中的行结构相同,MAAT会逐条加载。 | -| SEQUENCE_ REGION | 域ID生成序列号 | INTERGER | 用于生产者生成不重复的region_id | -| SEQUENCE_ GROUP | 分组ID生成序列号 | INTERGER | 用于生产者生成不重复的group_id | -| EXPIRE_OP_LOCK | 分布式锁 | 字符串”locked” | 用于保证最多只有一个写者进行淘汰。 | - -源码中reset_redis4maat.sh工具或Maat_cmd_flushDB函数可以对redis进行初始化。 - -#### 主版本号、预备版本号与Lua Script - -生产者写入配置时,先对预备版本号加1,并作为写入配置状态的score,待写入完成后,再对主版本号加1。放弃WATCH MAAT_VERSION的事务。这一方法可以大幅提高写入性能,除ID冲突外,可确保写入成功。 - -当有多个生产者时,可能存在配置状态与主版本号不一致的问题。主版本号为v,某次更新时在配置状态中声明的版本号为u,消费者增量更新时有以下情况: - -- 若v=u,则版本号一致,配置正常加载; -- 若v>u,该情况不存在。因为只有配置状态修改完成,主版本号才会增加1。换句话说,每次写入都是先增加预备版本号,后增加主版本号,所以主版本号必然小于或等于配置状态中的最大版本号。 - - 错误:三个生产者情况下,有问题,如下表。 -- 若vmv,本次更新的增量将在下一次 -- tvMaat版本+1,则说明有遗漏的更新(网络长时间中断),启用全量更新流程。 - -对于DEL状态,如果查询不到对应的主配置状态,同样说明有遗漏更新(网络中断时间超过OBSOLETE_RULE超时时间),启用全量更新流程。 - -#### MAAT_EXPIRE_TIMER - -该结构使用Sorted Set存储了主配置的超时信息,score为绝对超时时间,member的结构为TableName,ID。 - -删除配置时,exec_serial_rule会关联删除该索引。 - -#### MAAT_VERSION_TIMER - -该结构使用Sorted Set存储了每个版本的创建时间,score为版本创建时间,member为 版本号(version),即MAAT_UPDATE_STATUS的score,用以将MAAT_UPDATE_STATUS维持在一个较小的规模。 - -#### 主配置结构 - -有两类配置命名方式: - -1. EFFECTIVE_RULE:TableName,ID 表示正在生效的配置; -2. OBSOLETE_RULE:TableName,ID 表示已经删除的配置,这些配置超时(EXPIRE)后会被Redis删除。 - -### Load From Redis -Maat实例的工作线程定时轮询Redis中MAAT_VERSION,如果大于实例的MAAT_VERSION则进行更新。 - -![Load from Redis](./imgs/load-data-from-redis.png) - -### 读写性能 - -为保证事物,Redis需工作在单机+主从模式。带超时的配置写入5000条/秒,无超时配置10000条/秒。 - - -## JSON配置加载接口 +## 3. Json mode 使用Maat_summon_feather_jsonMaat_set_feather_opt函数通过选项MAAT_OPT_JSON_FILE_PATH设置,进行JSON格式配置的加载。Maat在初始化后,一旦检测到文件MD5值的变化,则以全量更新的方式加载变化的json文件。 @@ -457,10 +492,6 @@ Maat的配置管理线程会针对增量索引文件目录进行扫描,在初 文件扫描间隔和配置生效间隔,可以通过Maat_set_feather_opt设置,详见本文档“函数接口”一章。 -### 引用计数 - -引用计数机制为了避免多个变量多线程读写,因Cache一致性和伪共享问题导致速度降低,采用为每个线程分配64字节对齐的引用计数变量。 - ### 延迟删除机制 Maat使用延时删除机制,在不使用锁的前提下保证线程安全。 @@ -475,12 +506,4 @@ c) 需要修改时,获得mutex后访问 扫描线程中: -a) 需要读取时,获得mutex后访问 - -### 强制卸载机制 - -rulescan内部使用引用计数方式,管理待删除的自动机,其引用计数的加减周期是一次扫描函数的调用(而不是一次流式扫描)。满足MAAT实现强制卸载机制的条件。 - -所谓强制卸载机制,是指在一次流式扫描过程中,配置发生更新后,强制卸载该次扫描所引用的自动机,回收所占用内存。后续引用旧自动机的流式字符串扫描将不做任何命中,直接返回。 - -由于组合扫描为对MAAT的扫描器进行引用计数,替换前后各自使用当前的bool matcher,进行规则组合运算,不受此影响。 \ No newline at end of file +a) 需要读取时,获得mutex后访问 \ No newline at end of file diff --git a/docs/table_schema.md b/docs/table_schema.md index e4c9833..5d36757 100644 --- a/docs/table_schema.md +++ b/docs/table_schema.md @@ -1,21 +1,47 @@ # Table Schema -Since Maat 4.0,The range of item_id(group_id, compile_id) is 0~2^63,which is 8 bytes. +Maat tables are divided into two categories: physical tables that actually exist in the database and virtual tables that reference physical tables. -## Item Table +The types of physical tables are as follows: +- [item table](#1-item-table) +- [compile table](#4-compile-table) +- [group2compile table](#3-group2compile-table) +- [group2group table](#2-group2group-table) +- [plugin table](#5-plugin-table) +- [ip_plugin table](#6-ip_plugin-table) +- [fqdn_plugin table](#7-fqdn_plugin-table) +- [bool_plugin table](#8-bool_plugin-table) -Each item table must has the following columns +Different physical tables can be combined into one table, see [conjunction table](#112-12-conjunction-table) + +A virtual table can only reference one physical table or conjuntion table, see [virtual table](#111-11-virtual-table) + +## 1. Item table + +Item tables are further subdivided into different types of subtables as follows: +- [expr item table](#11-expr-item-table) +- [expr_plus item table](#12-expr_plus-item-table) +- [ip_plus item table](#13-ip_plus-item-table) +- [intval item table](#14-numeric-range-item-table) +- [intval_plus item table](#14-numeric-range-item-table) +- [flag item table](#15-flag-item-table) +- [flag_plus item table](#16-flag_plus-item-table) + +Each item table must has the following columns: + +- item_id: In a maat instance, the item id is globally unique, meaning that the item IDs of different tables must not be duplicate. -- item_id: In a maat instance, the item ID is globally unique, meaning that the item IDs of different tables must not be duplicate. - group_id: Indicate the group to which the item belongs, an item belongs to only one group. + - is_valid: In incremental updates, 1(valid means add) 0(invalid means del) -Different types of tables also have different fields defined according to their respective needs. +The range of item_id(group_id, compile_id) is 0~2^63,which is 8 bytes. -### 1. String item table +### 1.1 expr item table Describe matching rules for strings. -#### table schema +- table format + | **FieldName** | **type** | **NULL** | **constraint** | | ---------------- | -------------- | -------- | ------- | | **item_id** | LONG LONG | N | primary key | @@ -26,22 +52,78 @@ Describe matching rules for strings. | **is_hexbin** | INT | N | 0(not HEX & case insensitive, this is default value) 1(HEX & case sensitive) 2(not HEX & case sensitive) | | **is_valid** | INT | N | 0(invalid), 1(valid) | -Matching rules for string,expr_type column represents the expression type. +- table schema(stored in table_info.conf) +```c +{ + "table_id":3, //[0 ~ 1023], don't allow duplicate + "table_name":"HTTP_URL", //db table's name + "table_type":"expr", + "valid_column":7, //7th column(is_valid field) + "custom": { + "item_id":1, //1st column(item_id field) + "group_id":2, //2nd column(group_id field) + "keywords":3, //3rd column(keywords field) + "expr_type":4, //4th column(expr_type field) + "match_method":5,//5th column(match_method field) + "is_hexbin":6 //6th column(is_hexbin field) + } +} + +/* If you want to combine multiple physical tables into one table, db_tables should be added as follows. + The value of table_name can be a user-defined string, the value of db_tables is the table name that actually exists in database. */ +{ + "table_id":3, //[0 ~ 1023], don't allow duplicate + "table_name":"HTTP_REGION", //user-defined string + "db_tables":["HTTP_URL", "HTTP_HOST"], + "table_type":"expr", + "valid_column":7, + "custom": { + "item_id":1, + "group_id":2, + "keywords":3, + "expr_type":4, + "match_method":5, + "is_hexbin":6 + } +} +``` + +**expr_type** column represents the expression type: 1. keywords matching(0), match_method column as follows - substring matching (0) - - suffix matching (1) - - prefix matching (2) - - exactly matching (3) -2. AND expression(1), supports up to 8 substrings. + + For example: substring: "China", scan_data: "Hello China" will hit, "Hello World" will not hit + + - suffix matching (1) + + For example: suffix: ".baidu.com", scan_data: "www.baidu.com" will hit, "www.google.com" will not hit + + - prefix matching (2) + + For example: prefix: "^abc", scan_data: "abcdef" will hit, "1abcdef" will not hit + + - exactly matching (3) + + For example: string: "World", scan_data: "World" will hit, "Hello World" will not hit + +2. AND expression(1), supports up to 8 substrings. + + For example: AND expr: "yesterday&today", scan_data: "Goodbye yesterday, Hello today!" will hit, "Goodbye yesterday, Hello tomorrow!" will not hit. + 3. Regular expression(2) + + For example: Regex expr: "[W|world]", scan_data: "Hello world" will hit, "Hello World" will hit too. + 4. substring matching with offset(3) - offset start with 0, [offset_start, offset_end] closed interval + - multiple substrings with offset are logical AND -Since Maat4.0,only support UTF-8,no more encoding conversion。For binary format configurations, the keyword is hexadecimal, such as the keyword "hello" is represented as "68656C6C6F". A keyword can't contain invisible characters such as spaces, tabs, and CR, which are ASCII codes 0x00 to 0x1F and 0x7F. -If these characters need to be used, they must be escaped, refer to the "keywords escape table". -Characters led by backslashes outside this table are processed as ordinary strings, such as '\t' will be processed as the string "\t". + For example: substring expr: "1-1:48&3-4:4C4C", scan_data: "HELLO" will hit, "HLLO" will not hit. + **Note**: 48('H') 4C('L') + +  Since Maat4.0,only support UTF-8,no more encoding conversion。For binary format configurations, the keyword is hexadecimal, such as the keyword "hello" is represented as "68656C6C6F". A keyword can't contain invisible characters such as spaces, tabs, and CR, which are ASCII codes 0x00 to 0x1F and 0x7F. If these characters need to be used, they must be escaped, refer to the "keywords escape table". Characters led by backslashes outside this table are processed as ordinary strings, such as '\t' will be processed as the string "\t". The symbol '&' means conjunction operation in AND expression. So if the keywords has '&', it must be escaped by '\&'. @@ -55,17 +137,366 @@ The symbol '&' means conjunction operation in AND expression. So if the keywords Length constraint: -- Single substring no less than 3 bytes; -- No less than 3 bytes for a single substring in AND expression; +- Single substring no less than 3 bytes + +- No less than 3 bytes for a single substring in AND expression + - Support up to 8 substrings in one AND expression, expr = substr1 & substr2 & substr3 & substr4 & substr5 & substr6 & substr7 & substr8 + - The length of one AND expression should not exceed 1024 bytes(including '&') +Sample +- table schema +- rule +- scanning -### 2. IP item table +table schema stored in table_info.conf +```json +[ + { + "table_id":0, + "table_name":"COMPILE", + "table_type":"compile", + "valid_column":8, + "custom": { + "compile_id":1, + "tags":6, + "clause_num":9 + } + }, + { + "table_id":1, + "table_name":"GROUP2COMPILE", + "table_type":"group2compile", + "associated_compile_table_id":0, + "valid_column":3, + "custom": { + "group_id":1, + "compile_id":2, + "not_flag":4, + "virtual_table_name":5, + "clause_index":6 + } + }, + { + "table_id":2, + "table_name":"GROUP2GROUP", + "table_type":"group2group", + "valid_column":4, + "custom": { + "group_id":1, + "super_group_id":2, + "is_exclude":3 + } + }, + { + "table_id":3, + "table_name":"HTTP_URL", + "table_type":"expr", + "valid_column":7, + "custom": { + "item_id":1, + "group_id":2, + "keywords":3, + "expr_type":4, + "match_method":5, + "is_hexbin":6 + } + } + ] +``` + +rule stored in maat_json.json +```json +{ + "compile_table": "COMPILE", + "group2compile_table": "GROUP2COMPILE", + "group2group_table": "GROUP2GROUP", + "rules": [ + { + "compile_id": 123, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "anything", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": + { + "keywords": "multiple disciplines", + "expr_type": "none", + "match_method": "exact", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 124, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "anything", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": + { + "keywords": "baidu.com", + "expr_type": "none", + "match_method": "suffix", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 125, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "anything", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": + { + "keywords": "www", + "expr_type": "none", + "match_method": "prefix", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 126, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "anything", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": + { + "keywords": "abc&123", + "expr_type": "and", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 127, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "anything", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": + { + "keywords": "action=search\\&query=(.*)", + "expr_type": "regex", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 128, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "anything", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": + { + "keywords": "1-1:48&3-4:4C4C", + "expr_type": "offset", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + } + ] +} +``` + +scanning +```c +#include + +#include "maat.h" + +#define ARRAY_SIZE 16 + +const char *json_filename = "./maat_json.json"; +const char *table_info_path = "./table_info.conf"; + +int main() +{ + // initialize maat options which will be used by maat_new() + struct maat_options *opts = maat_options_new(); + maat_options_set_json_file(opts, json_filename); + maat_options_set_logger(opts, "./sample_test.log", LOG_LEVEL_INFO); + + // create maat instance, rules in table_info.conf will be loaded. + struct maat *maat_instance = maat_new(opts, table_info_path); + assert(maat_instance != NULL); + maat_options_free(opts); + + const char *table_name = "HTTP_URL"; //maat_json.json has HTTP_URL rule + int table_id = maat_get_table_id(maat_instance, table_name); + assert(table_id == 3); // defined in table_info.conf + + int thread_id = 0; + long long results[ARRAY_SIZE] = {0}; + size_t n_hit_result = 0; + + struct maat_state *state = maat_state_new(maat_instance, thread_id); + assert(state != NULL); + + const char *scan_data1 = "There are multiple disciplines"; + int ret = maat_scan_string(maat_instance, table_id, scan_data1, strlen(scan_data1), + results, ARRAY_SIZE, &n_hit_result, state); + assert(ret == MAAT_SCAN_HIT); + assert(n_hit_result == 1); + assert(results[0] == 123); + + maat_state_reset(state); + const char *scan_data2 = "www.baidu.com"; + ret = maat_scan_string(maat_instance, table_id, scan_data2, strlen(scan_data2), + results, ARRAY_SIZE, &n_hit_result, state); + assert(ret == MAAT_SCAN_HIT); + assert(n_hit_result == 1); + assert(results[0] == 124); + + maat_state_reset(state); + const char *scan_data3 = "www.google.com"; + ret = maat_scan_string(maat_instance, table_id, scan_data3, strlen(scan_data3), + results, ARRAY_SIZE, &n_hit_result, state); + assert(ret == MAAT_SCAN_HIT); + assert(n_hit_result == 1); + assert(results[0] == 125); + + maat_state_reset(state); + const char *scan_data4 = "alphabet abc, digit 123"; + ret = maat_scan_string(maat_instance, table_id, scan_data4, strlen(scan_data4), + results, ARRAY_SIZE, &n_hit_result, state); + assert(ret == MAAT_SCAN_HIT); + assert(n_hit_result == 1); + assert(results[0] == 126); + + maat_state_reset(state); + const char *scan_data5 = "http://www.cyberessays.com/search_results.php?action=search&query=username,abckkk,1234567"; + ret = maat_scan_string(maat_instance, table_id, scan_data5, strlen(scan_data5), + results, ARRAY_SIZE, &n_hit_result, state); + assert(ret == MAAT_SCAN_HIT); + assert(n_hit_result == 1); + assert(results[0] == 127); + + maat_state_reset(state); + const char *scan_data6 = "HELLO WORLD"; + ret = maat_scan_string(maat_instance, table_id, scan_data6, strlen(scan_data6), + results, ARRAY_SIZE, &n_hit_result, state); + assert(ret == MAAT_SCAN_HIT); + assert(n_hit_result == 1); + assert(results[0] == 128); + + maat_state_free(state); + + return 0; +} +``` + +### 1.2 expr_plus item table +Describe extended matching rules for strings by adding the district column. + +- table format + +| **FieldName** | **type** | **NULL** | **constraint** | +| ---------------- | -------------- | -------- | ------- | +| **item_id** | LONG LONG | N | primary key | +| **group_id** | LONG LONG | N | group2group or group2compile table's group_id | +| **district** | VARCHAR2(1024) | N | describe the effective position of the keywords | +| **keywords** | VARCHAR2(1024) | N | field to match during scanning | +| **expr_type** | INT | N | 0(keywords), 1(AND expr), 2(regular expr), 3(substring with offset) +| **match_method** | INT | N | only useful when expr_type is 0 | +| **is_hexbin** | INT | N | 0(not HEX & case insensitive, this is default value) 1(HEX & case sensitive) 2(not HEX & case sensitive) | +| **is_valid** | INT | N | 0(invalid), 1(valid) | + + +For example, if the district is User-Agent and keywords is Chrome, scanning in the following way will hit. +```c + const char *scan_data = "Chrome is fast"; + const char *district = "User-Agent"; + + maat_state_set_scan_district(..., district, ...); + maat_scan_string(..., scan_data, ...) +``` + +### 1.3 ip_plus item table Describe matching rules for IP address. Both the address and port are represented by string, IPv4 is dotted decimal and IPv6 is colon separated hexadecimal. -#### table schema +- table format | **FieldName** | **type** | **NULL** | **constraint** | | ------------- | ------------ | -------- | -------------- | @@ -81,26 +512,50 @@ Describe matching rules for IP address. Both the address and port are represente | protocol | INT | N | default(-1) TCP(6) UDP(17), user define field | | is_valid | INT | N | 0(invalid), 1(valid) | -### 3. Numeric item table +### 1.4 numeric range item table Determine whether an integer is within a certain numerical range. -#### table schema +- table format | **FieldName** | **type** | **NULL** | **constraint** | | ------------- | -------- | -------- | -------------- | -| item_id | INT | N | primary key | -| group_id | INT | N | group2group or group2compile table's group_id | -| low_boundary | INT | N | lower bound of the numerical range(including lb), 0 ~ (2^32 - 1)| -| up_boundary | INT | N | upper bound of the numerical range(including ub), 0 ~ (2^32 - 1)| -| is_valid | INT | N | 0(invalid), 1(valid) | +| item_id | INT | N | primary key | +| group_id | INT | N | group2group or group2compile table's group_id | +| low_boundary | INT | N | lower bound of the numerical range(including lb), 0 ~ (2^32 - 1)| +| up_boundary | INT | N | upper bound of the numerical range(including ub), 0 ~ (2^32 - 1)| +| is_valid | INT | N | 0(invalid), 1(valid) | +### 1.5 flag item table -### 4. Group2group table +- table format + +| **FieldName** | **type** | **NULL** | **constraint** | +| ------------- | -------- | -------- | -------------- | +| item_id | INT | N | primary key | +| group_id | INT | N | group2group or group2compile table's group_id | +| flag | INT | N | flag, 0 ~ (2^32 - 1)| +| flag_mask | INT | N | flag_mask, 0 ~ (2^32 - 1)| +| is_valid | INT | N | 0(invalid), 1(valid) | + +### 1.6 flag_plus item table + +- table format + +| **FieldName** | **type** | **NULL** | **constraint** | +| ------------- | -------- | -------- | -------------- | +| item_id | INT | N | primary key | +| group_id | INT | N | group2group or group2compile table's group_id | +| district | INT | N | describe the effective position of the flag | +| flag | INT | N | flag, 0 ~ (2^32 - 1)| +| flag_mask | INT | N | flag_mask, 0 ~ (2^32 - 1)| +| is_valid | INT | N | 0(invalid), 1(valid) | + +### 2. group2group table Describe the relationship between groups. -#### table schema +- table format | **FieldName** | **type** | **NULL** | **constraint** | | ----------------- | --------- | -------- | ---------------| @@ -109,11 +564,11 @@ Describe the relationship between groups. | is_exlude | Bool | N | 0(include) 1(exclude) | | is_valid | Bool | N | 0(invalid), 1(valid) | -### 5. Group2compile table +### 3. group2compile table Describe the relationship between group and compile. -#### table schema +- table format | **FieldName** | **type** | **NULL** | **constraint** | | ------------- | ------------- | -------- | ------- | @@ -126,11 +581,11 @@ Describe the relationship between group and compile. NOTE: If group_id is invalid in xx_item table, it must be marked as invalid in this table. -### 6. Compile table +### 4. compile table Describe the specific policy, One maat instance can has multiple compile tables with different names. -#### table schema +- table format | **FieldName** | **type** | **NULL** | **constraint** | | ---------------- | -------------- | -------- | --------------- | @@ -146,14 +601,54 @@ Describe the specific policy, One maat instance can has multiple compile tables | evaluation_order | DOUBLE | N | | default 0 | -### 7. Plugin table +### 5. plugin table -There is no fixed format for configuration of the plugin table, which is determined by business side. The plugin table support three types of keys: pointer, integer and ip_addr. +There is no fixed format for configuration of the plugin table, which is determined by business side. The plugin table supports two sets of callback functions, registered with **maat_table_callback_register** and **maat_plugin_table_ex_schema_register** respectively. + +maat_table_callback_register +```c +/* + When the plugin table configurations are updated, start will be called first and only once, then update will be called by each configuration item, and finish will be called last and only once. + + If configurations have been loaded but maat_table_callback_register has not yet been called, maat will cache the loaded configurations and perform the callbacks(start, update, finish) when registration is complete. +*/ + +typedef void maat_start_callback_t(int update_type, ...); +//table_line points to one complete configuration line, such as: "1\tHeBei\tShijiazhuang\t1\t0" +typedef void maat_update_callback_t(..., const char *table_line, ...); +typedef void maat_finish_callback_t(...); + +int maat_table_callback_register(..., + maat_start_callback_t *start, + maat_update_callback_t *update, + maat_finish_callback_t *finish, + ...); +``` + +maat_plugin_table_ex_schema_register + +```c +/* + +*/ + +typedef void maat_ex_new_func_t(..., const char *key, const char *table_line, ...); +typedef void maat_ex_free_func_t(...); +typedef void maat_ex_dup_func_t(...); + +int maat_plugin_table_ex_schema_register(..., + maat_ex_new_func_t *new_func, + maat_ex_free_func_t *free_func, + maat_ex_dup_func_t *dup_func, + ...); +``` + +three types of keys(pointer, integer and ip_addr) for ex_data callback. **pointer key(compatible with maat3)** (1) schema -``` +```json { "table_id":1, "table_name":"TEST_PLUGIN_POINTER_KEY_TYPE", @@ -168,7 +663,7 @@ There is no fixed format for configuration of the plugin table, which is determi ``` (2) plugin table configuration -``` +```json { "table_name": "TEST_PLUGIN_POINTER_KEY_TYPE", "table_content": [ @@ -180,7 +675,12 @@ There is no fixed format for configuration of the plugin table, which is determi } ``` -(3) get_ex_data +(3) register callback +```c + +``` + +(4) get ex_data ``` const char *key1 = "HeBei"; const char *table_name = "TEST_PLUGIN_POINTER_KEY_TYPE"; @@ -245,7 +745,7 @@ support integers of different lengths, such as int(4 bytes), long long(8 bytes). } ``` -(3) get_ex_data +(3) get ex_data ``` //int int key1 = 101; @@ -295,7 +795,7 @@ The addr_type column indicates whether the key is a v4 or v6 address. } ``` -(3) get_ex_data +(3) get ex_data ``` uint32_t ipv4_addr; inet_pton(AF_INET, "100.64.1.1", &ipv4_addr); @@ -306,11 +806,11 @@ maat_plugin_table_get_ex_data(maat_instance, table_id, (char *)&ipv4_addr, sizeo ``` -### 8. IP Plugin table +### 6. ip_plugin table Similar to plugin table but the key of maat_ip_plugin_table_get_ex_data is ip address. -### 9. FQDN Plugin table +### 7. fqdn_plugin table Scan the input string according to the domain name hierarchy '.' @@ -328,13 +828,13 @@ For example: If the input string is example.com.cn,则返回结果顺序为:3,1,2,4。规则5中的ample不是域名层级的一部分,不返回。 -### 10. BoolPlugin table +### 8. bool_plugin table 按照布尔表达式扫描输入的整数数组,如[100,1000,2,3]。 布尔表达式规则为“&”分隔的数字,例如“1&2&1000”。 -### 11. Virtual Table +### 1.11 virtual table 虚拟一个配置表,其内容为特定物理域配置表的视图。实践中,通常采用网络流量的属性作为虚拟表名,如HTTP_HOST、SSL_SNI等。一个虚拟表可以建立在多个不同类型的物理表之上,但不允许建立在其它虚拟表上。 @@ -345,7 +845,7 @@ For example: | **keyword_group_1** | compile_1 | 1 | 0 | 0 | REQUEST_BODY | | **keyword_group_1** | compile_1 | 1 | 0 | 0 | RESPONSE_BODY | -### 12. Conjunction Table +### 1.12 conjunction table 表名不同,但table id相同的表。旨在数据库表文件和MAAT API之间提供一个虚拟层,通过API调用一次扫描,即可扫描多张同类配置表。 @@ -358,7 +858,7 @@ For example: 支持所有类型表的连接,包括各类域配置、回调类配置。配置分组和配置编译的连接没有意义。 -## Foreign Files +## 2. Foreign Files 回调类配置中,特定字段可以指向一个外部内容,目前支持指向Redis中的一个key。 @@ -376,7 +876,7 @@ For example: ​ 内容外键的声明方法,参见本文档-配置表描述文件一节。 -## Tags +## 3. Tags 通过将Maat接受标签与配置标签的匹配,实现有选择的配置加载。其中配置标签是一个标签数组的集合,记为”tag_sets”,Maat接受标签是标签数组,记为”tags”。 diff --git a/docs/thread_mode.md b/docs/thread_mode.md new file mode 100644 index 0000000..f9cd6d8 --- /dev/null +++ b/docs/thread_mode.md @@ -0,0 +1,66 @@ +# Thread mode + +Maat will create a monitor loop thread internally when calling maat_new to create maat instance. Scaning threads are created extenal maat caller. So all maat_scan_xx APIs are per-thread + + +![The subordinate object](./imgs/thread_mode.png) + +Sample +```c + +const char *table_info_path = "table_info.conf"; +size_t thread_num = 5; + +struct thread_param { + int thread_id; + struct maat *maat_inst; + const char *table_name; +}; + +void *string_scan_thread(void *arg) +{ + struct thread_param *param = (struct thread_param *)arg; + struct maat *maat_inst = param->maat_inst; + const char *table_name = param->table_name; + const char *scan_data = "String TEST should hit"; + long long results[ARRAY_SIZE] = {0}; + size_t n_hit_result = 0; + + struct maat_state *state = maat_state_new(maat_inst, param->thread_id); + int table_id = maat_get_table_id(maat_inst, table_name); + + int ret = maat_scan_string(maat_inst, table_id, scan_data, strlen(scan_data), + results, ARRAY_SIZE, &n_hit_result, state); + EXPECT_EQ(ret, MAAT_SCAN_HIT); + EXPECT_EQ(n_hit_result, 1); + EXPECT_EQ(results[0], 123); + + maat_state_free(state); + + return NULL; +} + +int main() +{ + struct maat_options *opts = maat_options_new(); + maat_options_set_caller_thread_number(opts, thread_num); + struct maat *maat_inst = maat_new(opts, table_info_path); + + pthread_t threads[thread_num]; + struct thread_param thread_params[thread_num]; + + for (size_t i = 0; i < thread_num; i++) { + thread_params[i].maat_inst = maat_inst; + thread_params[i].thread_id = i; + thread_params[i].table_name = table_name; + pthread_create(&threads[i], NULL, string_scan_thread, thread_params+i); + } + + for (i = 0; i < thread_num; i++) { + pthread_join(threads[i], NULL); + } + + return 0; +} + +``` diff --git a/readme.md b/readme.md index 0b72b2d..a162863 100644 --- a/readme.md +++ b/readme.md @@ -195,4 +195,6 @@ int main() * [Scan API](./docs/scan_api.md) +* [Thread mode](./docs/thread_mode.md) + * [Tools](./docs/tools.md) \ No newline at end of file