diff options
-rw-r--r-- | CMakeLists.txt | 3 | ||||
-rw-r--r-- | conf/metrics.conf | 272 | ||||
-rw-r--r-- | rules/regexp/drugs.lua | 12 | ||||
-rw-r--r-- | rules/regexp/fraud.lua | 4 | ||||
-rw-r--r-- | rules/regexp/headers.lua | 511 | ||||
-rw-r--r-- | rules/regexp/lotto.lua | 2 | ||||
-rw-r--r-- | src/controller.c | 2 | ||||
-rw-r--r-- | src/libserver/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/libserver/cfg_file.h | 3 | ||||
-rw-r--r-- | src/libserver/cfg_utils.c | 1 | ||||
-rw-r--r-- | src/libserver/monitored.c | 2 | ||||
-rw-r--r-- | src/libserver/redis_pool.c | 378 | ||||
-rw-r--r-- | src/libserver/redis_pool.h | 70 | ||||
-rw-r--r-- | src/lua/lua_redis.c | 60 | ||||
-rw-r--r-- | src/plugins/lua/dmarc.lua | 83 | ||||
-rw-r--r-- | src/worker.c | 2 | ||||
-rw-r--r-- | test/functional/cases/150_rspamadm.robot | 9 |
17 files changed, 984 insertions, 431 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index e0be84d60..f0749f806 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -476,7 +476,8 @@ ENDMACRO() # Initial set # Prefer local include dirs to system ones -INCLUDE_DIRECTORIES("${CMAKE_SOURCE_DIR}/src" +INCLUDE_DIRECTORIES("${CMAKE_SOURCE_DIR}/" + "${CMAKE_SOURCE_DIR}/src" "${CMAKE_SOURCE_DIR}/src/libutil" "${CMAKE_SOURCE_DIR}/src/libserver" "${CMAKE_SOURCE_DIR}/src/libmime" diff --git a/conf/metrics.conf b/conf/metrics.conf index 0bab8ea51..1294ca2f1 100644 --- a/conf/metrics.conf +++ b/conf/metrics.conf @@ -28,99 +28,23 @@ metric { group "excessqp" { max_score = 2.4; - symbol "FROM_EXCESS_QP" { - weight = 1.2; - description = "From that contains encoded characters while quoted-printable is not needed as all symbols are 7bit"; - } - symbol "TO_EXCESS_QP" { - weight = 1.2; - description = "To that contains encoded characters while quoted-printable is not needed as all symbols are 7bit"; - } - symbol "REPLYTO_EXCESS_QP" { - weight = 1.2; - description = "Reply-To that contains encoded characters while quoted-printable is not needed as all symbols are 7bit"; - } - symbol "CC_EXCESS_QP" { - weight = 1.2; - description = "Cc that contains encoded characters while quoted-printable is not needed as all symbols are 7bit"; - } } group "excessb64" { max_score = 3.0; - symbol "FROM_EXCESS_BASE64" { - weight = 1.5; - description = "From that contains encoded characters while base 64 is not needed as all symbols are 7bit"; - } - symbol "TO_EXCESS_BASE64" { - weight = 1.5; - description = "To that contains encoded characters while base 64 is not needed as all symbols are 7bit"; - } - symbol "REPLYTO_EXCESS_BASE64" { - weight = 1.5; - description = "Reply-To that contains encoded characters while base 64 is not needed as all symbols are 7bit"; - } - symbol "CC_EXCESS_BASE64" { - weight = 1.5; - description = "Cc that contains encoded characters while base 64 is not needed as all symbols are 7bit"; - } } group "header" { - symbol "MISSING_SUBJECT" { - weight = 2.0; - description = "Subject is missing inside message"; - } - symbol "FORGED_OUTLOOK_TAGS" { - weight = 2.100000; - description = "Message pretends to be send from Outlook but has 'strange' tags "; - } symbol "FORGED_SENDER" { weight = 0.30; description = "Sender is forged (different From: header and smtp MAIL FROM: addresses)"; } - symbol "SUSPICIOUS_RECIPS" { - weight = 1.500000; - description = "Recipients seems to be autogenerated (works if recipients count is more than 5)"; - } symbol "MIME_HTML_ONLY" { weight = 0.2; description = "Messages that have only HTML part"; } - symbol "FORGED_MSGID_YAHOO" { - weight = 2.0; - description = "Forged yahoo msgid"; - } - symbol "FORGED_MUA_THEBAT_BOUN" { - weight = 2.0; - description = "Forged The Bat! MUA headers"; - } - symbol "R_MISSING_CHARSET" { - weight = 2.5; - description = "Charset is missing in a message"; - } - symbol "RCVD_DOUBLE_IP_SPAM" { - weight = 2.0; - description = "Two received headers with ip addresses"; - } - symbol "FORGED_OUTLOOK_HTML" { - weight = 5.0; - description = "Forged outlook HTML signature"; - } - symbol "R_UNDISC_RCPT" { - weight = 3.0; - description = "Recipients are absent or undisclosed"; - } symbol "FM_FAKE_HELO_VERIZON" { weight = 2.0; description = "Fake helo for verizon provider"; } - symbol "REPTO_QUOTE_YAHOO" { - weight = 2.0; - description = "Quoted reply-to from yahoo (seems to be forged)"; - } - symbol "MISSING_MIMEOLE" { - weight = 2.0; - description = "Mime-OLE is needed but absent (e.g. fake Outlook or fake Exchange)"; - } symbol "MISSING_TO" { weight = 2.0; description = "To header is missing"; @@ -135,42 +59,6 @@ metric { description = "Mixed characters in a URL inside message"; one_shot = true; } - symbol "SORTED_RECIPS" { - weight = 3.500000; - description = "Recipients list seems to be sorted"; - } - symbol "R_RCVD_SPAMBOTS" { - weight = 3.0; - description = "Spambots signatures in received headers"; - } - symbol "SUBJECT_NEEDS_ENCODING" { - weight = 1.0; - description = "Subject needs encoding"; - } - symbol "TRACKER_ID" { - weight = 3.84; - description = "Spam string at the end of message to make statistics faults 0"; - } - symbol "R_NO_SPACE_IN_FROM" { - weight = 1.0; - description = "No space in from header"; - } - symbol "R_SAJDING" { - weight = 8.0; - description = "Subject seems to be spam"; - } - symbol "R_BAD_CTE_7BIT" { - weight = 3.0; - description = "Detects bad content-transfer-encoding for text parts"; - } - symbol "INVALID_MSGID" { - weight = 1.7; - description = "Message id is incorrect"; - } - symbol "MISSING_MID" { - weight = 2.5; - description = "Message id is missing "; - } symbol "FORGED_RECIPIENTS" { weight = 2.0; description = "Recipients are not the same as RCPT TO: mail command"; @@ -183,14 +71,6 @@ metric { weight = 0.0; description = "Sender is not the same as MAIL FROM: envelope, but a message is from a maillist"; } - symbol "RATWARE_MS_HASH" { - weight = 2.0; - description = "Forged Exchange messages"; - } - symbol "STOX_REPLY_TYPE" { - weight = 1.0; - description = "Reply-type in content-type"; - } symbol "ONCE_RECEIVED" { weight = 0.1; description = "One received header in a message"; @@ -203,99 +83,15 @@ metric { weight = 4.0; description = "One received header with 'bad' patterns inside"; } - symbol "MIME_HEADER_CTYPE_ONLY" { - weight = 2.0; - description = "Only Content-Type header without other MIME headers"; - } symbol "MAILLIST" { weight = -0.2; description = "Message seems to be from maillist"; } - symbol "HEADER_FROM_DELIMITER_TAB" { - weight = 1.0; - description = "Header From begins with tab"; - } - symbol "HEADER_TO_DELIMITER_TAB" { - weight = 1.0; - description = "Header To begins with tab"; - } - symbol "HEADER_CC_DELIMITER_TAB" { - weight = 1.0; - description = "Header Cc begins with tab"; - } - symbol "HEADER_REPLYTO_DELIMITER_TAB" { - weight = 1.0; - description = "Header Reply-To begins with tab"; - } - symbol "HEADER_DATE_DELIMITER_TAB" { - weight = 1.0; - description = "Header Date begins with tab"; - } - symbol "HEADER_FROM_EMPTY_DELIMITER" { - weight = 1.0; - description = "Header From has no delimiter between header name and header value"; - } - symbol "HEADER_TO_EMPTY_DELIMITER" { - weight = 1.0; - description = "Header To has no delimiter between header name and header value"; - } - symbol "HEADER_CC_EMPTY_DELIMITER" { - weight = 1.0; - description = "Header Cc has no delimiter between header name and header value"; - } - symbol "HEADER_REPLYTO_EMPTY_DELIMITER" { - weight = 1.0; - description = "Header Reply-To has no delimiter between header name and header value"; - } - symbol "HEADER_DATE_EMPTY_DELIMITER" { - weight = 1.0; - description = "Header Date has no delimiter between header name and header value"; - } - symbol "RCVD_ILLEGAL_CHARS" { - weight = 4.0; - description = "Header Received has raw illegal character"; - } - symbol "FAKE_RECEIVED_mail_ru" { - weight = 4.0; - description = "Fake helo mail.ru in header Received from non mail.ru sender address"; - } - symbol "FAKE_RECEIVED_smtp_yandex_ru" { - weight = 4.0; - description = "Fake smtp.yandex.ru Received"; - } - symbol "FORGED_GENERIC_RECEIVED" { - weight = 3.6; - description = "Forged generic Received"; - } - symbol "FORGED_GENERIC_RECEIVED2" { - weight = 3.6; - description = "Forged generic Received"; - } - symbol "FORGED_GENERIC_RECEIVED3" { - weight = 3.6; - description = "Forged generic Received"; - } - symbol "FORGED_GENERIC_RECEIVED4" { - weight = 3.6; - description = "Forged generic Received"; - } - symbol "FORGED_GENERIC_RECEIVED5" { - weight = 4.6; - description = "Forged generic Received"; - } - symbol "INVALID_POSTFIX_RECEIVED" { - weight = 3.0; - description = "Invalid Postfix Received"; - } } group "subject" { max_score = 6.0; - symbol "FAKE_REPLY_C" { - weight = 6.0; - description = "Fake reply (has RE in subject, but has not References header)"; - } symbol "LONG_SUBJ" { weight = 6.0; description = "Subject is too long"; @@ -307,58 +103,6 @@ metric { } group "mua" { - symbol "FORGED_MUA_THEBAT_MSGID" { - weight = 4.0; - description = "Message pretends to be send from The Bat! but has forged Message-ID"; - } - symbol "FORGED_MUA_THEBAT_MSGID_UNKNOWN" { - weight = 3.0; - description = "Message pretends to be send from The Bat! but has forged Message-ID"; - } - symbol "FORGED_MUA_KMAIL_MSGID" { - weight = 3.0; - description = "Message pretends to be send from KMail but has forged Message-ID"; - } - symbol "FORGED_MUA_KMAIL_MSGID_UNKNOWN" { - weight = 2.5; - description = "Message pretends to be send from KMail but has forged Message-ID"; - } - symbol "FORGED_MUA_OPERA_MSGID" { - weight = 4.0; - description = "Message pretends to be send from Opera Mail but has forged Message-ID"; - } - symbol "SUSPICIOUS_OPERA_10W_MSGID" { - weight = 4.0; - description = "Message pretends to be send from suspicious Opera Mail/10.x (Windows) but has forged Message-ID, apparently from KMail"; - } - symbol "FORGED_MUA_MOZILLA_MAIL_MSGID" { - weight = 4.0; - description = "Message pretends to be send from Mozilla Mail but has forged Message-ID"; - } - symbol "FORGED_MUA_MOZILLA_MAIL_MSGID_UNKNOWN" { - weight = 2.5; - description = "Message pretends to be send from Mozilla Mail but has forged Message-ID"; - } - symbol "FORGED_MUA_THUNDERBIRD_MSGID" { - weight = 4.0; - description = "Forged mail pretending to be from Mozilla Thunderbird but has forged Message-ID"; - } - symbol "FORGED_MUA_THUNDERBIRD_MSGID_UNKNOWN" { - weight = 2.5; - description = "Forged mail pretending to be from Mozilla Thunderbird but has forged Message-ID"; - } - symbol "FORGED_MUA_SEAMONKEY_MSGID" { - weight = 4.0; - description = "Forged mail pretending to be from Mozilla Seamonkey but has forged Message-ID"; - } - symbol "FORGED_MUA_SEAMONKEY_MSGID_UNKNOWN" { - weight = 2.5; - description = "Forged mail pretending to be from Mozilla Seamonkey but has forged Message-ID"; - } - symbol "FORGED_MUA_OUTLOOK" { - weight = 3.0; - description = "Forged outlook MUA"; - } symbol "FORGED_MUA_MAILLIST" { weight = 0.0; description = "Avoid false positives for FORGED_MUA_* in maillist"; @@ -382,22 +126,6 @@ metric { weight = 0.5; description = "Short html part with a link to an image"; } - symbol "SUSPICIOUS_BOUNDARY" { - weight = 5.0; - description = "Suspicious boundary in header Content-Type"; - } - symbol "SUSPICIOUS_BOUNDARY2" { - weight = 4.0; - description = "Suspicious boundary in header Content-Type"; - } - symbol "SUSPICIOUS_BOUNDARY3" { - weight = 3.0; - description = "Suspicious boundary in header Content-Type"; - } - symbol "SUSPICIOUS_BOUNDARY4" { - weight = 4.0; - description = "Suspicious boundary in header Content-Type"; - } symbol "R_PARTS_DIFFER" { weight = 1.0; description = "Text and HTML parts differ"; diff --git a/rules/regexp/drugs.lua b/rules/regexp/drugs.lua index 7af31cd69..774c326a1 100644 --- a/rules/regexp/drugs.lua +++ b/rules/regexp/drugs.lua @@ -31,7 +31,7 @@ local drugs_diet7 = '/\\b_{0,3}t[_\\W]?[e3\\xE8-\\xEB][_\\W]?n[_\\W]?u[_\\W]?a[_ local drugs_diet8 = '/\\b_{0,3}d[_\\W]?[i1!|l\\xEC-\\xEF][_\\W]?d[_\\W]?r[_\\W][e3\\xE8-\\xEB[_\\W]?xx?_{0,3}\\b/irP' local drugs_diet9 = '/\\b_{0,3}a[_\\W]?d[_\\W]?[i1!|l\\xEC-\\xEF][_\\W]?p[_\\W]?[e3\\xE8-\\xEB][_\\W]?x_{0,3}\\b/irP' local drugs_diet10 = '/\\b_{0,3}x?x[_\\W]?[e3\\xE8-\\xEB][_\\W]?n[_\\W]?[i1!|l\\xEC-\\xEF][_\\W]?c[_\\W]?[a4\\xE0-\\xE6@][_\\W]?l_{0,3}\\b/irP' -reconf['DRUGS_DIET'] = string.format('((%s) | (%s) | (%s)) & ((%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s))', reconf['R_UNDISC_RCPT'], reconf['R_BAD_CTE_7BIT'], reconf['R_NO_SPACE_IN_FROM'], drugs_diet1, drugs_diet2, drugs_diet3, drugs_diet4, drugs_diet5, drugs_diet6, drugs_diet7, drugs_diet8, drugs_diet9, drugs_diet10) +reconf['DRUGS_DIET'] = string.format('((%s) | (%s) | (%s)) & ((%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s))', reconf['R_UNDISC_RCPT']['re'], reconf['R_BAD_CTE_7BIT']['re'], reconf['R_NO_SPACE_IN_FROM']['re'], drugs_diet1, drugs_diet2, drugs_diet3, drugs_diet4, drugs_diet5, drugs_diet6, drugs_diet7, drugs_diet8, drugs_diet9, drugs_diet10) local drugs_erectile1 = '/(?:\\b|\\s)[_\\W]{0,3}(?:\\\\\\/|V)[_\\W]{0,3}[ij1!|l\\xEC\\xED\\xEE\\xEF][_\\W]{0,3}[a40\\xE0-\\xE6@][_\\W]{0,3}[xyz]?[gj][_\\W]{0,3}r[_\\W]{0,3}[a40\\xE0-\\xE6@][_\\W]{0,3}x?[_\\W]{0,3}(?:\\b|\\s)/irP' local drugs_erectile2 = '/\\bV(?:agira|igara|iaggra|iaegra)\\b/irP' local drugs_erectile3 = '/(?:\\A|[\\s\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\x7f])[_\\W]{0,3}C[_\\W]{0,3}[ij1!|l\\xEC\\xED\\xEE\\xEF][_\\W]{0,3}[a4\\xE0-\\xE6@][_\\W]{0,3}l?[l!|1][_\\W]{0,3}[i1!|l\\xEC-\\xEF][_\\W]{0,3}s[_\\W]{0,3}(?:\\b|\\s)/irP' @@ -41,7 +41,7 @@ local drugs_erectile6 = '/\\b_{0,3}L[_\\W]?[e3\\xE8-\\xEB][_\\W]?(?:\\\\\\/|V)[_ local drugs_erectile8 = '/\\b_{0,3}T[_\\W]?[a4\\xE0-\\xE6@][_\\W]?d[_\\W]?[a4\\xE0-\\xE6@][_\\W]?l[_\\W]?[a4\\xE0-\\xE6@][_\\W]?f[_\\W]?[i1!|l\\xEC-\\xEF][_\\W]?l_{0,3}\\b/irP' local drugs_erectile10 = '/\\b_{0,3}V[_\\W]?(?:i|\\ï\\;)[_\\W]?(?:a|\\à|\\å)\\;?[_\\W]?g[_\\W]?r[_\\W]?(?:a|\\à|\\å)\\b/irP' local drugs_erectile11 = '/(?:\\b|\\s)_{0,3}[a4\\xE0-\\xE6@][_\\W]{0,3}p[_\\W]{0,3}c[_\\W]{0,3}[a4\\xE0-\\xE6@][_\\W]{0,3}[l!|1][_\\W]{0,3}[i1!|l\\xEC-\\xEF][_\\W]{0,3}s_{0,3}\\b/irP' -reconf['DRUGS_ERECTILE'] = string.format('((%s) | (%s) | (%s)) & ((%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s))', reconf['R_UNDISC_RCPT'], reconf['R_BAD_CTE_7BIT'], reconf['R_NO_SPACE_IN_FROM'], drugs_erectile1, drugs_erectile2, drugs_erectile3, drugs_erectile4, drugs_erectile5, drugs_erectile6, drugs_erectile8, drugs_erectile10, drugs_erectile11) +reconf['DRUGS_ERECTILE'] = string.format('((%s) | (%s) | (%s)) & ((%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s))', reconf['R_UNDISC_RCPT']['re'], reconf['R_BAD_CTE_7BIT']['re'], reconf['R_NO_SPACE_IN_FROM']['re'], drugs_erectile1, drugs_erectile2, drugs_erectile3, drugs_erectile4, drugs_erectile5, drugs_erectile6, drugs_erectile8, drugs_erectile10, drugs_erectile11) local drugs_anxiety1 = '/(?:\\b|\\s)[_\\W]{0,3}x?x[_\\W]{0,3}[a4\\xE0-\\xE6@][_\\W]{0,3}n[_\\W]{0,3}[ea4\\xE1\\xE2\\xE3@][_\\W]{0,3}xx?_{0,3}\\b/irP' local drugs_anxiety2 = '/\\bAlprazolam\\b/irP' local drugs_anxiety3 = '/(?:\\b|\\s)[_\\W]{0,3}(?:\\\\\\/|V)[_\\W]{0,3}[a4\\xE0-\\xE6@][_\\W]{0,3}[l|][_\\W]{0,3}[i1!|l\\xEC-\\xEF][_\\W]{0,3}[u\\xB5\\xF9-\\xFC][_\\W]{0,3}m\\b/irP' @@ -51,7 +51,7 @@ local drugs_anxiety6 = '/\\b_{0,3}l[_\\W]?[o0\\xF2-\\xF6][_\\W]?r[_\\W]?[a4\\xE0 local drugs_anxiety7 = '/\\b_{0,3}c[_\\W]?l[_\\W]?[o0\\xF2-\\xF6][_\\W]?n[_\\W]?[a4\\xE0-\\xE6@][_\\W]?z[_\\W]?e[_\\W]?p[_\\W]?[a4\\xE0-\\xE6@][_\\W]?m\\b/irP' local drugs_anxiety8 = '/\\bklonopin\\b/irP' local drugs_anxiety9 = '/\\brivotril\\b/irP' -reconf['DRUGS_ANXIETY'] = string.format('((%s) | (%s) | (%s)) & ((%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s))', reconf['R_UNDISC_RCPT'], reconf['R_BAD_CTE_7BIT'], reconf['R_NO_SPACE_IN_FROM'], drugs_anxiety1, drugs_anxiety2, drugs_anxiety3, drugs_anxiety4, drugs_anxiety5, drugs_anxiety6, drugs_anxiety7, drugs_anxiety8, drugs_anxiety9) +reconf['DRUGS_ANXIETY'] = string.format('((%s) | (%s) | (%s)) & ((%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s))', reconf['R_UNDISC_RCPT']['re'], reconf['R_BAD_CTE_7BIT']['re'], reconf['R_NO_SPACE_IN_FROM']['re'], drugs_anxiety1, drugs_anxiety2, drugs_anxiety3, drugs_anxiety4, drugs_anxiety5, drugs_anxiety6, drugs_anxiety7, drugs_anxiety8, drugs_anxiety9) reconf['DRUGS_ANXIETY_EREC'] = string.format('(%s) & (%s)', reconf['DRUGS_ERECTILE'], reconf['DRUGS_ANXIETY']) local drugs_pain1 = '/\\b_{0,3}h[_\\W]?y[_\\W]?d[_\\W]?r[_\\W]?[o0\\xF2-\\xF6][_\\W]?c[_\\W]?[o0\\xF2-\\xF6][_\\W]?d[_\\W]?[o0\\xF2-\\xF6][_\\W]?n[_\\W]?e_{0,3}\\b/irP' local drugs_pain2 = '/\\b_{0,3}c[o0\\xF2-\\xF6]deine_{0,3}\\b/irP' @@ -67,7 +67,7 @@ local drugs_pain11 = '/\\bzebutal\\b/irP' local drugs_pain12 = '/\\besgic plus\\b/irP' local drugs_pain13 = '/\\bD[_\\W]?[a4\\xE0-\\xE6@][_\\W]?r[_\\W]?v[_\\W]?[o0\\xF2-\\xF6][_\\W]?n\\b/irP' local drugs_pain14 = '/N[o0\\xF2-\\xF6]rc[o0\\xF2-\\xF6]/irP' -local drugs_pain = string.format('((%s) | (%s) | (%s)) & ((%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) || (%s) | (%s))', reconf['R_UNDISC_RCPT'], reconf['R_BAD_CTE_7BIT'], reconf['R_NO_SPACE_IN_FROM'], drugs_pain1, drugs_pain2, drugs_pain3, drugs_pain4, drugs_pain5, drugs_pain6, drugs_pain7, drugs_pain8, drugs_pain9, drugs_pain10, drugs_pain11, drugs_pain12, drugs_pain13, drugs_pain14) +local drugs_pain = string.format('((%s) | (%s) | (%s)) & ((%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) || (%s) | (%s))', reconf['R_UNDISC_RCPT']['re'], reconf['R_BAD_CTE_7BIT']['re'], reconf['R_NO_SPACE_IN_FROM']['re'], drugs_pain1, drugs_pain2, drugs_pain3, drugs_pain4, drugs_pain5, drugs_pain6, drugs_pain7, drugs_pain8, drugs_pain9, drugs_pain10, drugs_pain11, drugs_pain12, drugs_pain13, drugs_pain14) local drugs_sleep1 = '/(?:\\b|\\s)[_\\W]{0,3}[a4\\xE0-\\xE6@][_\\W]{0,3}m[_\\W]{0,3}b[_\\W]{0,3}[i1!|l\\xEC-\\xEF][_\\W]{0,3}[e3\\xE8-\\xEB][_\\W]{0,3}n[_\\W]{0,3}(?:\\b|\\s)/irP' local drugs_sleep2 = '/(?:\\b|\\s)[_\\W]{0,3}S[_\\W]{0,3}[o0\\xF2-\\xF6][_\\W]{0,3}n[_\\W]{0,3}[a4\\xE0-\\xE6@][_\\W]{0,3}t[_\\W]{0,3}[a4\\xE0-\\xE6@][_\\W]{0,3}(?:\\b|\\s)/irP' local drugs_sleep3 = '/\\b_{0,3}R[_\\W]?[e3\\xE8-\\xEB][_\\W]?s[_\\W]?t[_\\W]?[o0\\xF2-\\xF6][_\\W]?r[_\\W]?i[_\\W]?l_{0,3}\\b/irP' @@ -78,6 +78,6 @@ local drugs_muscle2 = '/\\b_{0,3}cycl[o0\\xF2-\\xF6]b[e3\\xE8-\\xEB]nz[a4\\xE0-\ local drugs_muscle3 = '/\\b_{0,3}f[_\\W]?l[_\\W]?[e3\\xE8-\\xEB][_\\W]?x[_\\W]?[e3\\xE8-\\xEB][_\\W]?r[_\\W]?[i1!|l\\xEC-\\xEF]_{0,3}[_\\W]?l_{0,3}\\b/irP' local drugs_muscle4 = '/\\b_{0,3}z[_\\W]?a[_\\W]?n[_\\W]?a[_\\W]?f[_\\W]?l[_\\W]?e[_\\W]?x_{0,3}\\b/irP' local drugs_muscle5 = '/\\bskelaxin\\b/irP' -reconf['DRUGS_MUSCLE'] = string.format('((%s) | (%s) | (%s)) & ((%s) | (%s) | (%s) | (%s) | (%s))', reconf['R_UNDISC_RCPT'], reconf['R_BAD_CTE_7BIT'], reconf['R_NO_SPACE_IN_FROM'], drugs_muscle1, drugs_muscle2, drugs_muscle3, drugs_muscle4, drugs_muscle5) -reconf['DRUGS_MANYKINDS'] = string.format('((%s) | (%s) | (%s)) & ((%s) + (%s) + (%s) + (%s) + (%s) + (%s) >= 3)', reconf['R_UNDISC_RCPT'], reconf['R_BAD_CTE_7BIT'], reconf['R_NO_SPACE_IN_FROM'], reconf['DRUGS_ERECTILE'], reconf['DRUGS_DIET'], drugs_pain, drugs_sleep, reconf['DRUGS_MUSCLE'], reconf['DRUGS_ANXIETY']) +reconf['DRUGS_MUSCLE'] = string.format('((%s) | (%s) | (%s)) & ((%s) | (%s) | (%s) | (%s) | (%s))', reconf['R_UNDISC_RCPT']['re'], reconf['R_BAD_CTE_7BIT']['re'], reconf['R_NO_SPACE_IN_FROM']['re'], drugs_muscle1, drugs_muscle2, drugs_muscle3, drugs_muscle4, drugs_muscle5) +reconf['DRUGS_MANYKINDS'] = string.format('((%s) | (%s) | (%s)) & ((%s) + (%s) + (%s) + (%s) + (%s) + (%s) >= 3)', reconf['R_UNDISC_RCPT']['re'], reconf['R_BAD_CTE_7BIT']['re'], reconf['R_NO_SPACE_IN_FROM']['re'], reconf['DRUGS_ERECTILE'], reconf['DRUGS_DIET'], drugs_pain, drugs_sleep, reconf['DRUGS_MUSCLE'], reconf['DRUGS_ANXIETY']) diff --git a/rules/regexp/fraud.lua b/rules/regexp/fraud.lua index 2571a8712..441aca5de 100644 --- a/rules/regexp/fraud.lua +++ b/rules/regexp/fraud.lua @@ -70,5 +70,5 @@ local fraud_yqv = '/nigerian? (?:national|government)/irP' local fraud_yja = '/over-invoice/irP' local fraud_ypo = '/the total sum/irP' local fraud_uoq = '/vital documents/irP' -reconf['ADVANCE_FEE_2'] = string.format('((%s) | (%s) | (%s)) & ((%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) >= 2)', reconf['R_UNDISC_RCPT'], reconf['R_BAD_CTE_7BIT'], reconf['R_NO_SPACE_IN_FROM'], fraud_kjv, fraud_irj, fraud_neb, fraud_xjr, fraud_ezy, fraud_zfj, fraud_kdt, fraud_bgp, fraud_fbi, fraud_jbu, fraud_jyg, fraud_xvw, fraud_snt, fraud_ltx, fraud_mcq, fraud_pvn, fraud_fvu, fraud_ckf, fraud_fcw, fraud_mqo, fraud_tcc, fraud_gbw, fraud_nrg, fraud_rlx, fraud_axf, fraud_thj, fraud_yqv, fraud_yja, fraud_ypo, fraud_uoq, fraud_dbi, fraud_bep, fraud_dpr, fraud_qxx, fraud_qfy, fraud_pts, fraud_tdp, fraud_gan, fraud_ipk, fraud_aon, fraud_wny, fraud_aum, fraud_wfc, fraud_yww, fraud_ulk, fraud_iou, fraud_jnb, fraud_irt, fraud_etx, fraud_wdr, fraud_uuy, fraud_mly) -reconf['ADVANCE_FEE_3'] = string.format('((%s) | (%s) | (%s)) & ((%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) >= 3)', reconf['R_UNDISC_RCPT'], reconf['R_BAD_CTE_7BIT'], reconf['R_NO_SPACE_IN_FROM'], fraud_kjv, fraud_irj, fraud_neb, fraud_xjr, fraud_ezy, fraud_zfj, fraud_kdt, fraud_bgp, fraud_fbi, fraud_jbu, fraud_jyg, fraud_xvw, fraud_snt, fraud_ltx, fraud_mcq, fraud_pvn, fraud_fvu, fraud_ckf, fraud_fcw, fraud_mqo, fraud_tcc, fraud_gbw, fraud_nrg, fraud_rlx, fraud_axf, fraud_thj, fraud_yqv, fraud_yja, fraud_ypo, fraud_uoq, fraud_dbi, fraud_bep, fraud_dpr, fraud_qxx, fraud_qfy, fraud_pts, fraud_tdp, fraud_gan, fraud_ipk, fraud_aon, fraud_wny, fraud_aum, fraud_wfc, fraud_yww, fraud_ulk, fraud_iou, fraud_jnb, fraud_irt, fraud_etx, fraud_wdr, fraud_uuy, fraud_mly) +reconf['ADVANCE_FEE_2'] = string.format('((%s) | (%s) | (%s)) & ((%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) >= 2)', reconf['R_UNDISC_RCPT']['re'], reconf['R_BAD_CTE_7BIT']['re'], reconf['R_NO_SPACE_IN_FROM']['re'], fraud_kjv, fraud_irj, fraud_neb, fraud_xjr, fraud_ezy, fraud_zfj, fraud_kdt, fraud_bgp, fraud_fbi, fraud_jbu, fraud_jyg, fraud_xvw, fraud_snt, fraud_ltx, fraud_mcq, fraud_pvn, fraud_fvu, fraud_ckf, fraud_fcw, fraud_mqo, fraud_tcc, fraud_gbw, fraud_nrg, fraud_rlx, fraud_axf, fraud_thj, fraud_yqv, fraud_yja, fraud_ypo, fraud_uoq, fraud_dbi, fraud_bep, fraud_dpr, fraud_qxx, fraud_qfy, fraud_pts, fraud_tdp, fraud_gan, fraud_ipk, fraud_aon, fraud_wny, fraud_aum, fraud_wfc, fraud_yww, fraud_ulk, fraud_iou, fraud_jnb, fraud_irt, fraud_etx, fraud_wdr, fraud_uuy, fraud_mly) +reconf['ADVANCE_FEE_3'] = string.format('((%s) | (%s) | (%s)) & ((%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) >= 3)', reconf['R_UNDISC_RCPT']['re'], reconf['R_BAD_CTE_7BIT']['re'], reconf['R_NO_SPACE_IN_FROM']['re'], fraud_kjv, fraud_irj, fraud_neb, fraud_xjr, fraud_ezy, fraud_zfj, fraud_kdt, fraud_bgp, fraud_fbi, fraud_jbu, fraud_jyg, fraud_xvw, fraud_snt, fraud_ltx, fraud_mcq, fraud_pvn, fraud_fvu, fraud_ckf, fraud_fcw, fraud_mqo, fraud_tcc, fraud_gbw, fraud_nrg, fraud_rlx, fraud_axf, fraud_thj, fraud_yqv, fraud_yja, fraud_ypo, fraud_uoq, fraud_dbi, fraud_bep, fraud_dpr, fraud_qxx, fraud_qfy, fraud_pts, fraud_tdp, fraud_gan, fraud_ipk, fraud_aon, fraud_wny, fraud_aum, fraud_wfc, fraud_yww, fraud_ulk, fraud_iou, fraud_jnb, fraud_irt, fraud_etx, fraud_wdr, fraud_uuy, fraud_mly) diff --git a/rules/regexp/headers.lua b/rules/regexp/headers.lua index 6ec37181f..b13274055 100644 --- a/rules/regexp/headers.lua +++ b/rules/regexp/headers.lua @@ -27,21 +27,35 @@ local subject_encoded_qp = 'Subject=/=\\?\\S+\\?Q\\?/iX' -- Define whether subject must be encoded (contains non-7bit characters) local subject_needs_mime = 'Subject=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/X' -- Final rule -reconf['SUBJECT_NEEDS_ENCODING'] = string.format('!(%s) & !(%s) & (%s)', subject_encoded_b64, subject_encoded_qp, subject_needs_mime) +reconf['SUBJECT_NEEDS_ENCODING'] = { + re = string.format('!(%s) & !(%s) & (%s)', subject_encoded_b64, subject_encoded_qp, subject_needs_mime), + score = 1.0, + description = 'Subject needs encoding', + group = 'header' +} -- Detects that there is no space in From header (e.g. Some Name<some@host>) -reconf['R_NO_SPACE_IN_FROM'] = 'From=/\\S<[-\\w\\.]+\\@[-\\w\\.]+>/X' +reconf['R_NO_SPACE_IN_FROM'] = { + re = 'From=/\\S<[-\\w\\.]+\\@[-\\w\\.]+>/X', + score = 1.0, + description = 'No space in from header', + group = 'header' +} +rspamd_config.MISSING_SUBJECT = { + score = 2.0, + description = 'Subject is missing inside message', + group = 'header', + callback = function(task) + local hdr = task:get_header('Subject') -rspamd_config.MISSING_SUBJECT = function(task) - local hdr = task:get_header('Subject') + if not hdr or #hdr == 0 then + return true + end - if not hdr or #hdr == 0 then - return true + return false end - - return false -end +} -- Detects bad content-transfer-encoding for text parts -- For text parts (text/plain and text/html mainly) @@ -50,27 +64,62 @@ local r_ctype_text = 'content_type_is_type(text)' local r_cte_7bit = 'compare_transfer_encoding(7bit)' -- And body contains 8bit characters local r_body_8bit = '/[^\\x01-\\x7f]/Pr' -reconf['R_BAD_CTE_7BIT'] = string.format('(%s) & (%s) & (%s)', r_ctype_text, r_cte_7bit, r_body_8bit) +reconf['R_BAD_CTE_7BIT'] = { + re = string.format('(%s) & (%s) & (%s)', r_ctype_text, r_cte_7bit, r_body_8bit), + score = 3.0, + description = 'Detects bad content-transfer-encoding for text parts', + group = 'header' +} -- Detects missing To header -reconf['MISSING_TO']= '!raw_header_exists(To)'; +reconf['MISSING_TO'] = { + re = '!raw_header_exists(To)', + score = 2.0, + description = 'To header is missing', + group = 'header' +} -- Detects undisclosed recipients local undisc_rcpt = 'To=/^<?undisclosed[- ]recipient/Hi' -reconf['R_UNDISC_RCPT'] = string.format('(%s)', undisc_rcpt) +reconf['R_UNDISC_RCPT'] = { + re = string.format('(%s)', undisc_rcpt), + score = 3.0, + description = 'Recipients are absent or undisclosed', + group = 'header' +} -- Detects missing Message-Id local has_mid = 'header_exists(Message-Id)' -reconf['MISSING_MID'] = '!header_exists(Message-Id)'; +reconf['MISSING_MID'] = { + re = '!header_exists(Message-Id)', + score = 2.5, + description = 'Message id is missing', + group = 'header' +} -- Received seems to be fake -reconf['R_RCVD_SPAMBOTS'] = 'Received=/^from \\[\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\] by [-.\\w+]{5,255}; [SMTWF][a-z][a-z], [\\s\\d]?\\d [JFMAJSOND][a-z][a-z] \\d{4} \\d{2}:\\d{2}:\\d{2} [-+]\\d{4}$/mH' +reconf['R_RCVD_SPAMBOTS'] = { + re = 'Received=/^from \\[\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\] by [-.\\w+]{5,255}; [SMTWF][a-z][a-z], [\\s\\d]?\\d [JFMAJSOND][a-z][a-z] \\d{4} \\d{2}:\\d{2}:\\d{2} [-+]\\d{4}$/mH', + score = 3.0, + description = 'Spambots signatures in received headers', + group = 'header' +} -- Charset is missing in message -reconf['R_MISSING_CHARSET']= string.format('content_type_is_type(text) & !content_type_has_param(charset) & !%s', r_cte_7bit); +reconf['R_MISSING_CHARSET'] = { + re = string.format('content_type_is_type(text) & !content_type_has_param(charset) & !%s', r_cte_7bit), + score = 2.5, + description = 'Charset is missing in a message', + group = 'header' +} -- Subject seems to be spam -reconf['R_SAJDING'] = 'Subject=/\\bsajding(?:om|a)?\\b/iH' +reconf['R_SAJDING'] = { + re = 'Subject=/\\bsajding(?:om|a)?\\b/iH', + score = 8.0, + description = 'Subject seems to be spam', + group = 'header' +} -- Find forged Outlook MUA -- Yahoo groups messages @@ -78,16 +127,36 @@ local yahoo_bulk = 'Received=/from \\[\\S+\\] by \\S+\\.(?:groups|scd|dcn)\\.yah -- Outlook MUA local outlook_mua = 'X-Mailer=/^Microsoft Outlook\\b/H' local any_outlook_mua = 'X-Mailer=/^Microsoft Outlook\\b/H' -reconf['FORGED_OUTLOOK_HTML'] = string.format('!%s & %s & %s', yahoo_bulk, outlook_mua, 'has_only_html_part()') +reconf['FORGED_OUTLOOK_HTML'] = { + re = string.format('!%s & %s & %s', yahoo_bulk, outlook_mua, 'has_only_html_part()'), + score = 5.0, + description = 'Forged outlook HTML signature', + group = 'header' +} -- Recipients seems to be likely with each other (only works when recipients count is more than 5 recipients) -reconf['SUSPICIOUS_RECIPS'] = 'compare_recipients_distance(0.65)' +reconf['SUSPICIOUS_RECIPS'] = { + re = 'compare_recipients_distance(0.65)', + score = 1.5, + description = 'Recipients seems to be autogenerated (works if recipients count is more than 5)', + group = 'header' +} -- Recipients list seems to be sorted -reconf['SORTED_RECIPS'] = 'is_recipients_sorted()' +reconf['SORTED_RECIPS'] = { + re = 'is_recipients_sorted()', + score = 3.5, + description = 'Recipients list seems to be sorted', + group = 'header' +} -- Spam string at the end of message to make statistics faults -reconf['TRACKER_ID'] = '/^[a-z0-9]{6,24}[-_a-z0-9]{12,36}[a-z0-9]{6,24}\\s*\\z/isPr' +reconf['TRACKER_ID'] = { + re = '/^[a-z0-9]{6,24}[-_a-z0-9]{12,36}[a-z0-9]{6,24}\\s*\\z/isPr', + score = 3.84, + description = 'Spam string at the end of message to make statistics fault', + group = 'header' +} -- From that contains encoded characters while base 64 is not needed as all symbols are 7bit @@ -96,13 +165,23 @@ local from_encoded_b64 = 'From=/\\=\\?\\S+\\?B\\?/iX' -- From contains only 7bit characters (parsed headers are used) local from_needs_mime = 'From=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/Hr' -- Final rule -reconf['FROM_EXCESS_BASE64'] = string.format('%s & !%s', from_encoded_b64, from_needs_mime) +reconf['FROM_EXCESS_BASE64'] = { + re = string.format('%s & !%s', from_encoded_b64, from_needs_mime), + score = 1.5, + description = 'From that contains encoded characters while base 64 is not needed as all symbols are 7bit', + group = 'excessb64' +} -- From that contains encoded characters while quoted-printable is not needed as all symbols are 7bit -- Regexp that checks that From header is encoded with quoted-printable (search in raw headers) local from_encoded_qp = 'From=/\\=\\?\\S+\\?Q\\?/iX' -- Final rule -reconf['FROM_EXCESS_QP'] = string.format('%s & !%s', from_encoded_qp, from_needs_mime) +reconf['FROM_EXCESS_QP'] = { + re = string.format('%s & !%s', from_encoded_qp, from_needs_mime), + score = 1.2, + description = 'From that contains encoded characters while quoted-printable is not needed as all symbols are 7bit', + group = 'excessqp' +} -- To that contains encoded characters while base 64 is not needed as all symbols are 7bit -- Regexp that checks that To header is encoded with base64 (search in raw headers) @@ -110,13 +189,23 @@ local to_encoded_b64 = 'To=/\\=\\?\\S+\\?B\\?/iX' -- To contains only 7bit characters (parsed headers are used) local to_needs_mime = 'To=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/Hr' -- Final rule -reconf['TO_EXCESS_BASE64'] = string.format('%s & !%s', to_encoded_b64, to_needs_mime) +reconf['TO_EXCESS_BASE64'] = { + re = string.format('%s & !%s', to_encoded_b64, to_needs_mime), + score = 1.5, + description = 'To that contains encoded characters while base 64 is not needed as all symbols are 7bit', + group = 'excessb64' +} -- To that contains encoded characters while quoted-printable is not needed as all symbols are 7bit -- Regexp that checks that To header is encoded with quoted-printable (search in raw headers) local to_encoded_qp = 'To=/\\=\\?\\S+\\?Q\\?/iX' -- Final rule -reconf['TO_EXCESS_QP'] = string.format('%s & !%s', to_encoded_qp, to_needs_mime) +reconf['TO_EXCESS_QP'] = { + re = string.format('%s & !%s', to_encoded_qp, to_needs_mime), + score = 1.2, + description = 'To that contains encoded characters while quoted-printable is not needed as all symbols are 7bit', + group = 'excessqp' +} -- Reply-To that contains encoded characters while base 64 is not needed as all symbols are 7bit -- Regexp that checks that Reply-To header is encoded with base64 (search in raw headers) @@ -124,13 +213,23 @@ local replyto_encoded_b64 = 'Reply-To=/\\=\\?\\S+\\?B\\?/iX' -- Reply-To contains only 7bit characters (parsed headers are used) local replyto_needs_mime = 'Reply-To=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/Hr' -- Final rule -reconf['REPLYTO_EXCESS_BASE64'] = string.format('%s & !%s', replyto_encoded_b64, replyto_needs_mime) +reconf['REPLYTO_EXCESS_BASE64'] = { + re = string.format('%s & !%s', replyto_encoded_b64, replyto_needs_mime), + score = 1.5, + description = 'Reply-To that contains encoded characters while base 64 is not needed as all symbols are 7bit', + group = 'excessb64' +} -- Reply-To that contains encoded characters while quoted-printable is not needed as all symbols are 7bit -- Regexp that checks that Reply-To header is encoded with quoted-printable (search in raw headers) local replyto_encoded_qp = 'Reply-To=/\\=\\?\\S+\\?Q\\?/iX' -- Final rule -reconf['REPLYTO_EXCESS_QP'] = string.format('%s & !%s', replyto_encoded_qp, replyto_needs_mime) +reconf['REPLYTO_EXCESS_QP'] = { + re = string.format('%s & !%s', replyto_encoded_qp, replyto_needs_mime), + score = 1.2, + description = 'Reply-To that contains encoded characters while quoted-printable is not needed as all symbols are 7bit', + group = 'excessqp' +} -- Cc that contains encoded characters while base 64 is not needed as all symbols are 7bit -- Regexp that checks that Cc header is encoded with base64 (search in raw headers) @@ -138,13 +237,23 @@ local cc_encoded_b64 = 'Cc=/\\=\\?\\S+\\?B\\?/iX' -- Co contains only 7bit characters (parsed headers are used) local cc_needs_mime = 'Cc=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/Hr' -- Final rule -reconf['CC_EXCESS_BASE64'] = string.format('%s & !%s', cc_encoded_b64, cc_needs_mime) +reconf['CC_EXCESS_BASE64'] = { + re = string.format('%s & !%s', cc_encoded_b64, cc_needs_mime), + score = 1.5, + description = 'Cc that contains encoded characters while base 64 is not needed as all symbols are 7bit', + group = 'excessb64' +} -- Cc that contains encoded characters while quoted-printable is not needed as all symbols are 7bit -- Regexp that checks that Cc header is encoded with quoted-printable (search in raw headers) local cc_encoded_qp = 'Cc=/\\=\\?\\S+\\?Q\\?/iX' -- Final rule -reconf['CC_EXCESS_QP'] = string.format('%s & !%s', cc_encoded_qp, cc_needs_mime) +reconf['CC_EXCESS_QP'] = { + re = string.format('%s & !%s', cc_encoded_qp, cc_needs_mime), + score = 1.2, + description = 'Cc that contains encoded characters while quoted-printable is not needed as all symbols are 7bit', + group = 'excessqp' +} -- Detect forged outlook headers @@ -182,8 +291,13 @@ local forged_outlook_dollars = string.format('(%s & !%s & !%s & !%s & !%s & !%s) local fmo_excl_o3416 = 'X-Mailer=/^Microsoft Outlook, Build 10.0.3416$/H' local fmo_excl_oe3790 = 'X-Mailer=/^Microsoft Outlook Express 6.00.3790.3959$/H' -- Summary rule for forged outlook -reconf['FORGED_MUA_OUTLOOK'] = string.format('(%s | %s) & !%s & !%s & !%s', - forged_oe, forged_outlook_dollars, fmo_excl_o3416, fmo_excl_oe3790, vista_msgid) +reconf['FORGED_MUA_OUTLOOK'] = { + re = string.format('(%s | %s) & !%s & !%s & !%s', + forged_oe, forged_outlook_dollars, fmo_excl_o3416, fmo_excl_oe3790, vista_msgid), + score = 3.0, + description = 'Forged outlook MUA', + group = 'mua' +} -- HTML outlook signs local mime_html = 'content_type_is_type(text) & content_type_is_subtype(/.?html/)' @@ -191,20 +305,45 @@ local tag_exists_html = 'has_html_tag(html)' local tag_exists_head = 'has_html_tag(head)' local tag_exists_meta = 'has_html_tag(meta)' local tag_exists_body = 'has_html_tag(body)' -reconf['FORGED_OUTLOOK_TAGS'] = string.format('!%s & %s & %s & !(%s & %s & %s & %s)', - yahoo_bulk, any_outlook_mua, mime_html, tag_exists_html, tag_exists_head, - tag_exists_meta, tag_exists_body) +reconf['FORGED_OUTLOOK_TAGS'] = { + re = string.format('!%s & %s & %s & !(%s & %s & %s & %s)', + yahoo_bulk, any_outlook_mua, mime_html, tag_exists_html, tag_exists_head, + tag_exists_meta, tag_exists_body), + score = 2.1, + description = "Message pretends to be send from Outlook but has 'strange' tags", + group = 'header' +} -- Forged OE/MSO boundary -reconf['SUSPICIOUS_BOUNDARY'] = 'Content-Type=/^\\s*multipart.+boundary="----=_NextPart_000_[A-Z\\d]{4}_(00EBFFA4|0102FFA4|32C6FFA4|3302FFA4)\\.[A-Z\\d]{8}"[\\r\\n]*$/siX' +reconf['SUSPICIOUS_BOUNDARY'] = { + re = 'Content-Type=/^\\s*multipart.+boundary="----=_NextPart_000_[A-Z\\d]{4}_(00EBFFA4|0102FFA4|32C6FFA4|3302FFA4)\\.[A-Z\\d]{8}"[\\r\\n]*$/siX', + score = 5.0, + description = 'Suspicious boundary in header Content-Type', + group = 'mua' +} -- Forged OE/MSO boundary -reconf['SUSPICIOUS_BOUNDARY2'] = 'Content-Type=/^\\s*multipart.+boundary="----=_NextPart_000_[A-Z\\d]{4}_(01C6527E)\\.[A-Z\\d]{8}"[\\r\\n]*$/siX' +reconf['SUSPICIOUS_BOUNDARY2'] = { + re = 'Content-Type=/^\\s*multipart.+boundary="----=_NextPart_000_[A-Z\\d]{4}_(01C6527E)\\.[A-Z\\d]{8}"[\\r\\n]*$/siX', + score = 4.0, + description = 'Suspicious boundary in header Content-Type', + group = 'mua' +} -- Forged OE/MSO boundary -reconf['SUSPICIOUS_BOUNDARY3'] = 'Content-Type=/^\\s*multipart.+boundary="-----000-00\\d\\d-01C[\\dA-F]{5}-[\\dA-F]{8}"[\\r\\n]*$/siX' +reconf['SUSPICIOUS_BOUNDARY3'] = { + re = 'Content-Type=/^\\s*multipart.+boundary="-----000-00\\d\\d-01C[\\dA-F]{5}-[\\dA-F]{8}"[\\r\\n]*$/siX', + score = 3.0, + description = 'Suspicious boundary in header Content-Type', + group = 'mua' +} -- Forged OE/MSO boundary local suspicious_boundary_01C4 = 'Content-Type=/^\\s*multipart.+boundary="----=_NextPart_000_[A-Z\\d]{4}_01C4[\\dA-F]{4}\\.[A-Z\\d]{8}"[\\r\\n]*$/siX' local suspicious_boundary_01C4_date = 'Date=/^\\s*\\w\\w\\w,\\s+\\d+\\s+\\w\\w\\w 20(0[56789]|1\\d)/' -reconf['SUSPICIOUS_BOUNDARY4'] = string.format('(%s) & (%s)', suspicious_boundary_01C4, suspicious_boundary_01C4_date) +reconf['SUSPICIOUS_BOUNDARY4'] = { + re = string.format('(%s) & (%s)', suspicious_boundary_01C4, suspicious_boundary_01C4_date), + score = 4.0, + description = 'Suspicious boundary in header Content-Type', + group = 'mua' +} -- Detect forged The Bat! headers -- The Bat! X-Mailer header @@ -214,10 +353,19 @@ local thebat_msgid_common = 'Message-ID=/^<?\\d+\\.\\d+\\@\\S+>?$/mH' -- Correct The Bat! Message-ID template local thebat_msgid = 'Message-ID=/^<?\\d+\\.(19[789]\\d|20\\d\\d)(0\\d|1[012])([012]\\d|3[01])([0-5]\\d)([0-5]\\d)([0-5]\\d)\\@\\S+>?/mH' -- Summary rule for forged The Bat! Message-ID header -reconf['FORGED_MUA_THEBAT_MSGID'] = string.format('(%s) & !(%s) & (%s) & !(%s)', thebat_mua_any, thebat_msgid, thebat_msgid_common, unusable_msgid) +reconf['FORGED_MUA_THEBAT_MSGID'] = { + re = string.format('(%s) & !(%s) & (%s) & !(%s)', thebat_mua_any, thebat_msgid, thebat_msgid_common, unusable_msgid), + score = 4.0, + description = 'Message pretends to be send from The Bat! but has forged Message-ID', + group = 'mua' +} -- Summary rule for forged The Bat! Message-ID header with unknown template -reconf['FORGED_MUA_THEBAT_MSGID_UNKNOWN'] = string.format('(%s) & !(%s) & !(%s) & !(%s)', thebat_mua_any, thebat_msgid, thebat_msgid_common, unusable_msgid) - +reconf['FORGED_MUA_THEBAT_MSGID_UNKNOWN'] = { + re = string.format('(%s) & !(%s) & !(%s) & !(%s)', thebat_mua_any, thebat_msgid, thebat_msgid_common, unusable_msgid), + score = 3.0, + description = 'Message pretends to be send from The Bat! but has forged Message-ID', + group = 'mua' +} -- Detect forged KMail headers -- KMail User-Agent header @@ -235,9 +383,19 @@ function kmail_msgid (task) return false end -- Summary rule for forged KMail Message-ID header -reconf['FORGED_MUA_KMAIL_MSGID'] = string.format('(%s) & (%s) & !(%s) & !(%s)', kmail_mua, kmail_msgid_common, 'kmail_msgid', unusable_msgid) +reconf['FORGED_MUA_KMAIL_MSGID'] = { + re = string.format('(%s) & (%s) & !(%s) & !(%s)', kmail_mua, kmail_msgid_common, 'kmail_msgid', unusable_msgid), + score = 3.0, + description = 'Message pretends to be send from KMail but has forged Message-ID', + group = 'mua' +} -- Summary rule for forged KMail Message-ID header with unknown template -reconf['FORGED_MUA_KMAIL_MSGID_UNKNOWN'] = string.format('(%s) & !(%s) & !(%s)', kmail_mua, kmail_msgid_common, unusable_msgid) +reconf['FORGED_MUA_KMAIL_MSGID_UNKNOWN'] = { + re = string.format('(%s) & !(%s) & !(%s)', kmail_mua, kmail_msgid_common, unusable_msgid), + score = 2.5, + description = 'Message pretends to be send from KMail but has forged Message-ID', + group = 'mua' +} -- Detect forged Opera Mail headers -- Opera Mail User-Agent header @@ -249,10 +407,19 @@ local suspicious_opera10w_mua = 'User-Agent=/^\\s*Opera Mail\\/10\\.\\d+ \\(Wind -- Suspicious Opera Mail Message-ID, apparently from KMail local suspicious_opera10w_msgid = 'Message-Id=/^<?2009\\d{8}\\.\\d+\\.\\S+\\@\\S+?>$/H' -- Summary rule for forged Opera Mail User-Agent header and Message-ID header from KMail -reconf['SUSPICIOUS_OPERA_10W_MSGID'] = string.format('(%s) & (%s)', suspicious_opera10w_mua, suspicious_opera10w_msgid) +reconf['SUSPICIOUS_OPERA_10W_MSGID'] = { + re = string.format('(%s) & (%s)', suspicious_opera10w_mua, suspicious_opera10w_msgid), + score = 4.0, + description = 'Message pretends to be send from suspicious Opera Mail/10.x (Windows) but has forged Message-ID, apparently from KMail', + group = 'mua' +} -- Summary rule for forged Opera Mail Message-ID header -reconf['FORGED_MUA_OPERA_MSGID'] = string.format('(%s) & !(%s) & !(%s) & !(%s)', opera1x_mua, opera1x_msgid, reconf['SUSPICIOUS_OPERA_10W_MSGID'], unusable_msgid) - +reconf['FORGED_MUA_OPERA_MSGID'] = { + re = string.format('(%s) & !(%s) & !(%s) & !(%s)', opera1x_mua, opera1x_msgid, reconf['SUSPICIOUS_OPERA_10W_MSGID']['re'], unusable_msgid), + score = 4.0, + description = 'Message pretends to be send from Opera Mail but has forged Message-ID', + group = 'mua' +} -- Detect forged Mozilla Mail/Thunderbird/Seamonkey headers -- Mozilla based X-Mailer @@ -265,20 +432,56 @@ local mozilla_msgid_common = 'Message-ID=/^\\s*<[\\dA-F]{8}\\.\\d{1,7}\\@([^>\\. local mozilla_msgid_common_sec = 'Message-ID=/^\\s*<[\\da-f]{8}-([\\da-f]{4}-){3}[\\da-f]{12}\\@([^>\\.]+\\.)+[^>\\.]+>$/H' local mozilla_msgid = 'Message-ID=/^\\s*<(3[3-9A-F]|4[\\dA-F]|5[\\dA-F])[\\dA-F]{6}\\.(\\d0){1,4}\\d\\@([^>\\.]+\\.)+[^>\\.]+>$/H' -- Summary rule for forged Mozilla Mail Message-ID header -reconf['FORGED_MUA_MOZILLA_MAIL_MSGID'] = string.format('(%s) & (%s) & !(%s) & !(%s)', user_agent_mozilla, mozilla_msgid_common, mozilla_msgid, unusable_msgid) -reconf['FORGED_MUA_MOZILLA_MAIL_MSGID_UNKNOWN'] = string.format('(%s) & !(%s) & !(%s) & !(%s)', user_agent_mozilla, mozilla_msgid_common, mozilla_msgid, unusable_msgid) +reconf['FORGED_MUA_MOZILLA_MAIL_MSGID'] = { + re = string.format('(%s) & (%s) & !(%s) & !(%s)', user_agent_mozilla, mozilla_msgid_common, mozilla_msgid, unusable_msgid), + score = 4.0, + description = 'Message pretends to be send from Mozilla Mail but has forged Message-ID', + group = 'mua' +} +reconf['FORGED_MUA_MOZILLA_MAIL_MSGID_UNKNOWN'] = { + re = string.format('(%s) & !(%s) & !(%s) & !(%s)', user_agent_mozilla, mozilla_msgid_common, mozilla_msgid, unusable_msgid), + score = 2.5, + description = 'Message pretends to be send from Mozilla Mail but has forged Message-ID', + group = 'mua' +} + -- Summary rule for forged Thunderbird Message-ID header -reconf['FORGED_MUA_THUNDERBIRD_MSGID'] = string.format('(%s) & (%s) & !(%s) & !(%s)', user_agent_thunderbird, mozilla_msgid_common, mozilla_msgid, unusable_msgid) -reconf['FORGED_MUA_THUNDERBIRD_MSGID_UNKNOWN'] = string.format('(%s) & !((%s) | (%s)) & !(%s) & !(%s)', user_agent_thunderbird, mozilla_msgid_common, mozilla_msgid_common_sec, mozilla_msgid, unusable_msgid) +reconf['FORGED_MUA_THUNDERBIRD_MSGID'] = { + re = string.format('(%s) & (%s) & !(%s) & !(%s)', user_agent_thunderbird, mozilla_msgid_common, mozilla_msgid, unusable_msgid), + score = 4.0, + description = 'Forged mail pretending to be from Mozilla Thunderbird but has forged Message-ID', + group = 'mua' +} +reconf['FORGED_MUA_THUNDERBIRD_MSGID_UNKNOWN'] = { + re = string.format('(%s) & !((%s) | (%s)) & !(%s) & !(%s)', user_agent_thunderbird, mozilla_msgid_common, mozilla_msgid_common_sec, mozilla_msgid, unusable_msgid), + score = 2.5, + description = 'Forged mail pretending to be from Mozilla Thunderbird but has forged Message-ID', + group = 'mua' +} -- Summary rule for forged Seamonkey Message-ID header -reconf['FORGED_MUA_SEAMONKEY_MSGID'] = string.format('(%s) & (%s) & !(%s) & !(%s)', user_agent_seamonkey, mozilla_msgid_common, mozilla_msgid, unusable_msgid) -reconf['FORGED_MUA_SEAMONKEY_MSGID_UNKNOWN'] = string.format('(%s) & !(%s) & !(%s) & !(%s)', user_agent_seamonkey, mozilla_msgid_common, mozilla_msgid, unusable_msgid) +reconf['FORGED_MUA_SEAMONKEY_MSGID'] = { + re = string.format('(%s) & (%s) & !(%s) & !(%s)', user_agent_seamonkey, mozilla_msgid_common, mozilla_msgid, unusable_msgid), + score = 4.0, + description = 'Forged mail pretending to be from Mozilla Seamonkey but has forged Message-ID', + group = 'mua' +} +reconf['FORGED_MUA_SEAMONKEY_MSGID_UNKNOWN'] = { + re = string.format('(%s) & !(%s) & !(%s) & !(%s)', user_agent_seamonkey, mozilla_msgid_common, mozilla_msgid, unusable_msgid), + score = 2.5, + description = 'Forged mail pretending to be from Mozilla Seamonkey but has forged Message-ID', + group = 'mua' +} -- Message id validity local sane_msgid = 'Message-Id=/^<?[^<>\\\\ \\t\\n\\r\\x0b\\x80-\\xff]+\\@[^<>\\\\ \\t\\n\\r\\x0b\\x80-\\xff]+>?\\s*$/H' local msgid_comment = 'Message-Id=/\\(.*\\)/H' -reconf['INVALID_MSGID'] = string.format('(%s) & !((%s) | (%s))', has_mid, sane_msgid, msgid_comment) +reconf['INVALID_MSGID'] = { + re = string.format('(%s) & !((%s) | (%s))', has_mid, sane_msgid, msgid_comment), + score = 1.7, + description = 'Message id is incorrect', + group = 'header' +} -- Only Content-Type header without other MIME headers @@ -287,17 +490,32 @@ local cte = 'header_exists(Content-Transfer-Encoding)' local ct = 'header_exists(Content-Type)' local mime_version = 'raw_header_exists(MIME-Version)' local ct_text_plain = 'content_type_is_type(text) & content_type_is_subtype(plain)' -reconf['MIME_HEADER_CTYPE_ONLY'] = string.format('!(%s) & !(%s) & (%s) & !(%s) & !(%s)', cd, cte, ct, mime_version, ct_text_plain) +reconf['MIME_HEADER_CTYPE_ONLY'] = { + re = string.format('!(%s) & !(%s) & (%s) & !(%s) & !(%s)', cd, cte, ct, mime_version, ct_text_plain), + score = 2.0, + description = 'Only Content-Type header without other MIME headers', + group = 'header' +} -- Forged Exchange messages local msgid_dollars_ok = 'Message-Id=/[0-9a-f]{4,}\\$[0-9a-f]{4,}\\$[0-9a-f]{4,}\\@\\S+/H' local mimeole_ms = 'X-MimeOLE=/^Produced By Microsoft MimeOLE/H' local rcvd_with_exchange = 'Received=/with Microsoft Exchange Server/H' -reconf['RATWARE_MS_HASH'] = string.format('(%s) & !(%s) & !(%s)', msgid_dollars_ok, mimeole_ms, rcvd_with_exchange) +reconf['RATWARE_MS_HASH'] = { + re = string.format('(%s) & !(%s) & !(%s)', msgid_dollars_ok, mimeole_ms, rcvd_with_exchange), + score = 2.0, + description = 'Forged Exchange messages', + group = 'header' +} -- Reply-type in content-type -reconf['STOX_REPLY_TYPE'] = 'Content-Type=/text\\/plain; .* reply-type=original/H' +reconf['STOX_REPLY_TYPE'] = { + re = 'Content-Type=/text\\/plain; .* reply-type=original/H', + score = 1.0, + description = 'Reply-type in content-type', + group = 'header' +} -- Fake Verizon headers local fhelo_verizon = 'X-Spam-Relays-Untrusted=/^[^\\]]+ helo=[^ ]+verizon\\.net /iH' @@ -308,7 +526,12 @@ reconf['FM_FAKE_HELO_VERIZON'] = string.format('(%s) & !(%s)', fhelo_verizon, fh local at_yahoo_msgid = 'Message-Id=/\\@yahoo\\.com\\b/iH' local at_yahoogroups_msgid = 'Message-Id=/\\@yahoogroups\\.com\\b/iH' local from_yahoo_com = 'From=/\\@yahoo\\.com\\b/iH' -reconf['FORGED_MSGID_YAHOO'] = string.format('(%s) & !(%s)', at_yahoo_msgid, from_yahoo_com) +reconf['FORGED_MSGID_YAHOO'] = { + re = string.format('(%s) & !(%s)', at_yahoo_msgid, from_yahoo_com), + score = 2.0, + description = 'Forged yahoo msgid', + group = 'header' +} local r_from_yahoo_groups = 'From=/rambler.ru\\@returns\\.groups\\.yahoo\\.com\\b/iH' local r_from_yahoo_groups_ro = 'From=/ro.ru\\@returns\\.groups\\.yahoo\\.com\\b/iH' @@ -317,18 +540,33 @@ local thebat_mua_v1 = 'X-Mailer=/^The Bat! \\(v1\\./H' local ctype_has_boundary = 'Content-Type=/boundary/iH' local bat_boundary = 'Content-Type=/boundary=\\"?-{10}/H' local mailman_21 = 'X-Mailman-Version=/\\d/H' -reconf['FORGED_MUA_THEBAT_BOUN'] = string.format('(%s) & (%s) & !(%s) & !(%s)', thebat_mua_v1, ctype_has_boundary, bat_boundary, mailman_21) +reconf['FORGED_MUA_THEBAT_BOUN'] = { + re = string.format('(%s) & (%s) & !(%s) & !(%s)', thebat_mua_v1, ctype_has_boundary, bat_boundary, mailman_21), + score = 2.0, + description = 'Forged The Bat! MUA headers', + group = 'header' +} -- Two received headers with ip addresses local double_ip_spam_1 = 'Received=/from \\[\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\] by \\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3} with/H' local double_ip_spam_2 = 'Received=/from\\s+\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\s+by\\s+\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3};/H' -reconf['RCVD_DOUBLE_IP_SPAM'] = string.format('(%s) | (%s)', double_ip_spam_1, double_ip_spam_2) +reconf['RCVD_DOUBLE_IP_SPAM'] = { + re = string.format('(%s) | (%s)', double_ip_spam_1, double_ip_spam_2), + score = 2.0, + description = 'Two received headers with ip addresses', + group = 'header' +} -- Quoted reply-to from yahoo (seems to be forged) local repto_quote = 'Reply-To=/\\".*\\"\\s*\\</H' local from_yahoo_com = 'From=/\\@yahoo\\.com\\b/iH' local at_yahoo_msgid = 'Message-Id=/\\@yahoo\\.com\\b/iH' -reconf['REPTO_QUOTE_YAHOO'] = string.format('(%s) & ((%s) | (%s))', repto_quote, from_yahoo_com, at_yahoo_msgid) +reconf['REPTO_QUOTE_YAHOO'] = { + re = string.format('(%s) & ((%s) | (%s))', repto_quote, from_yahoo_com, at_yahoo_msgid), + score = 2.0, + description = 'Quoted reply-to from yahoo (seems to be forged)', + group = 'header' +} -- MUA definitions local xm_gnus = 'X-Mailer=/^Gnus v/H' @@ -349,18 +587,28 @@ local subj_re = 'Subject=/^R[eE]:/H' local has_ref = 'header_exists(References)' local missing_ref = string.format('!(%s)', has_ref) -- Fake reply (has RE in subject, but has not References header) -reconf['FAKE_REPLY_C'] = string.format('(%s) & (%s) & (%s) & !(%s)', subj_re, missing_ref, no_inr_yes_ref, xm_msoe6) +reconf['FAKE_REPLY_C'] = { + re = string.format('(%s) & (%s) & (%s) & !(%s)', subj_re, missing_ref, no_inr_yes_ref, xm_msoe6), + score = 6.0, + description = 'Fake reply (has RE in subject, but has not References header)', + group = 'subject' +} -- Mime-OLE is needed but absent (e.g. fake Outlook or fake Ecxchange) local has_msmail_pri = 'header_exists(X-MSMail-Priority)' local has_mimeole = 'header_exists(X-MimeOLE)' local has_squirrelmail_in_mailer = 'X-Mailer=/SquirrelMail\\b/H' local has_office12145_in_mailer = 'X-Mailer=/^Microsoft (?:Office )?Outlook 1[245]\\.0/' -reconf['MISSING_MIMEOLE'] = string.format('(%s) & !(%s) & !(%s) & !(%s)', - has_msmail_pri, - has_mimeole, - has_squirrelmail_in_mailer, - has_office12145_in_mailer) +reconf['MISSING_MIMEOLE'] = { + re = string.format('(%s) & !(%s) & !(%s) & !(%s)', + has_msmail_pri, + has_mimeole, + has_squirrelmail_in_mailer, + has_office12145_in_mailer), + score = 2.0, + description = 'Mime-OLE is needed but absent (e.g. fake Outlook or fake Exchange)', + group = 'header' +} -- Header delimiters local yandex_from = 'From=/\\@(yandex\\.ru|yandex\\.net|ya\\.ru)/iX' @@ -375,11 +623,36 @@ function check_header_delimiter_tab(task, header_name) end return false end -reconf['HEADER_FROM_DELIMITER_TAB'] = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(From)', yandex) -reconf['HEADER_TO_DELIMITER_TAB'] = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(To)', yandex) -reconf['HEADER_CC_DELIMITER_TAB'] = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(Cc)', yandex) -reconf['HEADER_REPLYTO_DELIMITER_TAB'] = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(Reply-To)', yandex) -reconf['HEADER_DATE_DELIMITER_TAB'] = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(Date)', yandex) +reconf['HEADER_FROM_DELIMITER_TAB'] = { + re = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(From)', yandex), + score = 1.0, + description = 'Header From begins with tab', + group = 'header' +} +reconf['HEADER_TO_DELIMITER_TAB'] = { + re = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(To)', yandex), + score = 1.0, + description = 'Header To begins with tab', + group = 'header' +} +reconf['HEADER_CC_DELIMITER_TAB'] = { + re = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(Cc)', yandex), + score = 1.0, + description = 'Header To begins with tab', + group = 'header' +} +reconf['HEADER_REPLYTO_DELIMITER_TAB'] = { + re = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(Reply-To)', yandex), + score = 1.0, + description = 'Header Reply-To begins with tab', + group = 'header' +} +reconf['HEADER_DATE_DELIMITER_TAB'] = { + re = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(Date)', yandex), + score = 1.0, + description = 'Header Date begins with tab', + group = 'header' +} -- Empty delimiters between header names and header values function check_header_delimiter_empty(task, header_name) for _,rh in ipairs(task:get_header_full(header_name)) do @@ -387,21 +660,56 @@ function check_header_delimiter_empty(task, header_name) end return false end -reconf['HEADER_FROM_EMPTY_DELIMITER'] = string.format('(%s)', 'check_header_delimiter_empty(From)') -reconf['HEADER_TO_EMPTY_DELIMITER'] = string.format('(%s)', 'check_header_delimiter_empty(To)') -reconf['HEADER_CC_EMPTY_DELIMITER'] = string.format('(%s)', 'check_header_delimiter_empty(Cc)') -reconf['HEADER_REPLYTO_EMPTY_DELIMITER'] = string.format('(%s)', 'check_header_delimiter_empty(Reply-To)') -reconf['HEADER_DATE_EMPTY_DELIMITER'] = string.format('(%s)', 'check_header_delimiter_empty(Date)') +reconf['HEADER_FROM_EMPTY_DELIMITER'] = { + re = string.format('(%s)', 'check_header_delimiter_empty(From)'), + score = 1.0, + description = 'Header From has no delimiter between header name and header value', + group = 'header' +} +reconf['HEADER_TO_EMPTY_DELIMITER'] = { + re = string.format('(%s)', 'check_header_delimiter_empty(To)'), + score = 1.0, + description = 'Header To has no delimiter between header name and header value', + group = 'header' +} +reconf['HEADER_CC_EMPTY_DELIMITER'] = { + re = string.format('(%s)', 'check_header_delimiter_empty(Cc)'), + score = 1.0, + description = 'Header Cc has no delimiter between header name and header value', + group = 'header' +} +reconf['HEADER_REPLYTO_EMPTY_DELIMITER'] = { + re = string.format('(%s)', 'check_header_delimiter_empty(Reply-To)'), + score = 1.0, + description = 'Header Reply-To has no delimiter between header name and header value', + group = 'header' +} +reconf['HEADER_DATE_EMPTY_DELIMITER'] = { + re = string.format('(%s)', 'check_header_delimiter_empty(Date)'), + score = 1.0, + description = 'Header Date has no delimiter between header name and header value', + group = 'header' +} -- Definitions of received headers regexp -reconf['RCVD_ILLEGAL_CHARS'] = 'Received=/[\\x80-\\xff]/X' +reconf['RCVD_ILLEGAL_CHARS'] = { + re = 'Received=/[\\x80-\\xff]/X', + score = 4.0, + description = 'Header Received has raw illegal character', + group = 'header' +} local MAIL_RU_Return_Path = 'Return-path=/^\\s*<.+\\@mail\\.ru>$/iX' local MAIL_RU_X_Envelope_From = 'X-Envelope-From=/^\\s*<.+\\@mail\\.ru>$/iX' local MAIL_RU_From = 'From=/\\@mail\\.ru>?$/iX' local MAIL_RU_Received = 'Received=/from mail\\.ru \\(/mH' -reconf['FAKE_RECEIVED_mail_ru'] = string.format('(%s) & !(((%s) | (%s)) & (%s))', MAIL_RU_Received, MAIL_RU_Return_Path, MAIL_RU_X_Envelope_From, MAIL_RU_From) +reconf['FAKE_RECEIVED_mail_ru'] = { + re = string.format('(%s) & !(((%s) | (%s)) & (%s))', MAIL_RU_Received, MAIL_RU_Return_Path, MAIL_RU_X_Envelope_From, MAIL_RU_From), + score = 4.0, + description = 'Fake helo mail.ru in header Received from non mail.ru sender address', + group = 'header' +} local GMAIL_COM_Return_Path = 'Return-path=/^\\s*<.+\\@gmail\\.com>$/iX' local GMAIL_COM_X_Envelope_From = 'X-Envelope-From=/^\\s*<.+\\@gmail\\.com>$/iX' @@ -421,19 +729,54 @@ local RECEIVED_smtp_yandex_ru_7 = 'Received=/from \\S+ \\(HELO smtp\\.yandex\\.r local RECEIVED_smtp_yandex_ru_8 = 'Received=/from \\S+ \\(HELO smtp\\.yandex\\.ru\\) \\(\\d+\\.\\d+\\.\\d+\\.\\d+\\)/iX' local RECEIVED_smtp_yandex_ru_9 = 'Received=/from \\S+ \\(\\[\\d+\\.\\d+\\.\\d+\\.\\d+\\] helo=smtp\\.yandex\\.ru\\)/iX' -reconf['FAKE_RECEIVED_smtp_yandex_ru'] = string.format('(((%s) & ((%s) | (%s))) | ((%s) & ((%s) | (%s))) | ((%s) & ((%s) | (%s)))) & (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s)', MAIL_RU_From, MAIL_RU_Return_Path, MAIL_RU_X_Envelope_From, GMAIL_COM_From, GMAIL_COM_Return_Path, GMAIL_COM_X_Envelope_From, UKR_NET_From, UKR_NET_Return_Path, UKR_NET_X_Envelope_From, RECEIVED_smtp_yandex_ru_1, RECEIVED_smtp_yandex_ru_2, RECEIVED_smtp_yandex_ru_3, RECEIVED_smtp_yandex_ru_4, RECEIVED_smtp_yandex_ru_5, RECEIVED_smtp_yandex_ru_6, RECEIVED_smtp_yandex_ru_7, RECEIVED_smtp_yandex_ru_8, RECEIVED_smtp_yandex_ru_9) +reconf['FAKE_RECEIVED_smtp_yandex_ru'] = { + re = string.format('(((%s) & ((%s) | (%s))) | ((%s) & ((%s) | (%s))) | ((%s) & ((%s) | (%s)))) & (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s)', MAIL_RU_From, MAIL_RU_Return_Path, MAIL_RU_X_Envelope_From, GMAIL_COM_From, GMAIL_COM_Return_Path, GMAIL_COM_X_Envelope_From, UKR_NET_From, UKR_NET_Return_Path, UKR_NET_X_Envelope_From, RECEIVED_smtp_yandex_ru_1, RECEIVED_smtp_yandex_ru_2, RECEIVED_smtp_yandex_ru_3, RECEIVED_smtp_yandex_ru_4, RECEIVED_smtp_yandex_ru_5, RECEIVED_smtp_yandex_ru_6, RECEIVED_smtp_yandex_ru_7, RECEIVED_smtp_yandex_ru_8, RECEIVED_smtp_yandex_ru_9), + score = 4.0, + description = 'Fake smtp.yandex.ru Received', + group = 'header' +} -reconf['FORGED_GENERIC_RECEIVED'] = 'Received=/^\\s*(.+\\n)*from \\[\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\] by (([\\w\\d-]+\\.)+[a-zA-Z]{2,6}|\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}); \\w{3}, \\d+ \\w{3} 20\\d\\d \\d\\d\\:\\d\\d\\:\\d\\d [+-]\\d\\d\\d0/X' +reconf['FORGED_GENERIC_RECEIVED'] = { + re = 'Received=/^\\s*(.+\\n)*from \\[\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\] by (([\\w\\d-]+\\.)+[a-zA-Z]{2,6}|\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}); \\w{3}, \\d+ \\w{3} 20\\d\\d \\d\\d\\:\\d\\d\\:\\d\\d [+-]\\d\\d\\d0/X', + score = 3.6, + description = 'Forged generic Received', + group = 'header' +} -reconf['FORGED_GENERIC_RECEIVED2'] = 'Received=/^\\s*(.+\\n)*from \\[\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\] by ([\\w\\d-]+\\.)+[a-z]{2,6} id [\\w\\d]{12}; \\w{3}, \\d+ \\w{3} 20\\d\\d \\d\\d\\:\\d\\d\\:\\d\\d [+-]\\d\\d\\d0/X' +reconf['FORGED_GENERIC_RECEIVED2'] = { + re = 'Received=/^\\s*(.+\\n)*from \\[\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\] by ([\\w\\d-]+\\.)+[a-z]{2,6} id [\\w\\d]{12}; \\w{3}, \\d+ \\w{3} 20\\d\\d \\d\\d\\:\\d\\d\\:\\d\\d [+-]\\d\\d\\d0/X', + score = 3.6, + description = 'Forged generic Received', + group = 'header' +} -reconf['FORGED_GENERIC_RECEIVED3'] = 'Received=/^\\s*(.+\\n)*by \\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3} with SMTP id [a-zA-Z]{14}\\.\\d{13};[\\r\\n\\s]*\\w{3}, \\d+ \\w{3} 20\\d\\d \\d\\d\\:\\d\\d\\:\\d\\d [+-]\\d\\d\\d0 \\(GMT\\)/X' +reconf['FORGED_GENERIC_RECEIVED3'] = { + re = 'Received=/^\\s*(.+\\n)*by \\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3} with SMTP id [a-zA-Z]{14}\\.\\d{13};[\\r\\n\\s]*\\w{3}, \\d+ \\w{3} 20\\d\\d \\d\\d\\:\\d\\d\\:\\d\\d [+-]\\d\\d\\d0 \\(GMT\\)/X', + score = 3.6, + description = 'Forged generic Received', + group = 'header' +} -reconf['FORGED_GENERIC_RECEIVED4'] = 'Received=/^\\s*(.+\\n)*from localhost by \\S+;\\s+\\w{3}, \\d+ \\w{3} 20\\d\\d \\d\\d\\:\\d\\d\\:\\d\\d [+-]\\d\\d\\d0[\\s\\r\\n]*$/X' +reconf['FORGED_GENERIC_RECEIVED4'] = { + re = 'Received=/^\\s*(.+\\n)*from localhost by \\S+;\\s+\\w{3}, \\d+ \\w{3} 20\\d\\d \\d\\d\\:\\d\\d\\:\\d\\d [+-]\\d\\d\\d0[\\s\\r\\n]*$/X', + score = 3.6, + description = 'Forged generic Received', + group = 'header' +} -reconf['FORGED_GENERIC_RECEIVED5'] = 'Received=/\\s*from \\[(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\\].*\\n(.+\\n)*\\s*from \\1 by \\S+;\\s+\\w{3}, \\d+ \\w{3} 20\\d\\d \\d\\d\\:\\d\\d\\:\\d\\d [+-]\\d\\d\\d0$/X' +reconf['FORGED_GENERIC_RECEIVED5'] = { + re = 'Received=/\\s*from \\[(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\\].*\\n(.+\\n)*\\s*from \\1 by \\S+;\\s+\\w{3}, \\d+ \\w{3} 20\\d\\d \\d\\d\\:\\d\\d\\:\\d\\d [+-]\\d\\d\\d0$/X', + score = 4.6, + description = 'Forged generic Received', + group = 'header' +} -reconf['INVALID_POSTFIX_RECEIVED'] = 'Received=/ \\(Postfix\\) with ESMTP id [A-Z\\d]+([\\s\\r\\n]+for <\\S+?>)?;[\\s\\r\\n]*[A-Z][a-z]{2}, \\d{1,2} [A-Z][a-z]{2} \\d\\d\\d\\d \\d\\d:\\d\\d:\\d\\d [\\+\\-]\\d\\d\\d\\d$/X' +reconf['INVALID_POSTFIX_RECEIVED'] = { + re = 'Received=/ \\(Postfix\\) with ESMTP id [A-Z\\d]+([\\s\\r\\n]+for <\\S+?>)?;[\\s\\r\\n]*[A-Z][a-z]{2}, \\d{1,2} [A-Z][a-z]{2} \\d\\d\\d\\d \\d\\d:\\d\\d:\\d\\d [\\+\\-]\\d\\d\\d\\d$/X', + score = 3.0, + description = 'Invalid Postfix Received', + group = 'header' +} reconf['X_PHP_EVAL'] = { re = "X-PHP-Originating-Script=/ : eval\\(\\)'d code$/X", diff --git a/rules/regexp/lotto.lua b/rules/regexp/lotto.lua index c5cdff4c6..df0f2577a 100644 --- a/rules/regexp/lotto.lua +++ b/rules/regexp/lotto.lua @@ -28,4 +28,4 @@ local kam_lotto3 = '/(won|claim|cash prize|pounds? sterling)/isrP' local kam_lotto4 = '/(claims (officer|agent)|lottery coordinator|fiduciary (officer|agent)|fiduaciary claims)/isrP' local kam_lotto5 = '/(freelotto group|Royal Heritage Lottery|UK National (Online)? Lottery|U\\.?K\\.? Grand Promotions|Lottery Department UK|Euromillion Loteria|Luckyday International Lottery|International Lottery)/isrP' local kam_lotto6 = '/(Dear Lucky Winner|Winning Notification|Attention:Winner|Dear Winner)/isrP' -reconf['R_LOTTO'] = string.format('((%s) | (%s) | (%s)) & (((%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s)) >= 3)', reconf['R_UNDISC_RCPT'], reconf['R_BAD_CTE_7BIT'], reconf['R_NO_SPACE_IN_FROM'], r_lotto_from, r_lotto_subject, r_lotto_body, kam_lotto1, kam_lotto2, kam_lotto3, kam_lotto4, kam_lotto5, kam_lotto6) +reconf['R_LOTTO'] = string.format('((%s) | (%s) | (%s)) & (((%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s) + (%s)) >= 3)', reconf['R_UNDISC_RCPT']['re'], reconf['R_BAD_CTE_7BIT']['re'], reconf['R_NO_SPACE_IN_FROM']['re'], r_lotto_from, r_lotto_subject, r_lotto_body, kam_lotto1, kam_lotto2, kam_lotto3, kam_lotto4, kam_lotto5, kam_lotto6) diff --git a/src/controller.c b/src/controller.c index 7280d9951..2b19a7dd7 100644 --- a/src/controller.c +++ b/src/controller.c @@ -2836,6 +2836,8 @@ start_controller_worker (struct rspamd_worker *worker) rspamd_upstreams_library_config (worker->srv->cfg, worker->srv->cfg->ups_ctx, ctx->ev_base, ctx->resolver->r); + rspamd_redis_pool_config (worker->srv->cfg->redis_pool, + worker->srv->cfg, ctx->ev_base); /* Maps events */ rspamd_map_watch (worker->srv->cfg, ctx->ev_base, ctx->resolver); rspamd_symbols_cache_start_refresh (worker->srv->cfg->cache, ctx->ev_base); diff --git a/src/libserver/CMakeLists.txt b/src/libserver/CMakeLists.txt index 49e4e6d25..295ad59c8 100644 --- a/src/libserver/CMakeLists.txt +++ b/src/libserver/CMakeLists.txt @@ -13,6 +13,7 @@ SET(LIBRSPAMDSERVERSRC ${CMAKE_CURRENT_SOURCE_DIR}/monitored.c ${CMAKE_CURRENT_SOURCE_DIR}/protocol.c ${CMAKE_CURRENT_SOURCE_DIR}/proxy.c + ${CMAKE_CURRENT_SOURCE_DIR}/redis_pool.c ${CMAKE_CURRENT_SOURCE_DIR}/re_cache.c ${CMAKE_CURRENT_SOURCE_DIR}/roll_history.c ${CMAKE_CURRENT_SOURCE_DIR}/spf.c diff --git a/src/libserver/cfg_file.h b/src/libserver/cfg_file.h index 48c3c812f..7ce7f98a4 100644 --- a/src/libserver/cfg_file.h +++ b/src/libserver/cfg_file.h @@ -16,6 +16,8 @@ #include "libserver/re_cache.h" #include "ref.h" #include "libutil/radix.h" +#include "monitored.h" +#include "redis_pool.h" #define DEFAULT_BIND_PORT 11333 #define DEFAULT_CONTROL_PORT 11334 @@ -406,6 +408,7 @@ struct rspamd_config { struct rspamd_external_libs_ctx *libs_ctx; /**< context for external libraries */ struct rspamd_monitored_ctx *monitored_ctx; /**< context for monitored resources */ + struct rspamd_redis_pool *redis_pool; /**< redis connectiosn pool */ struct rspamd_re_cache *re_cache; /**< static regexp cache */ diff --git a/src/libserver/cfg_utils.c b/src/libserver/cfg_utils.c index decd33156..a50986c80 100644 --- a/src/libserver/cfg_utils.c +++ b/src/libserver/cfg_utils.c @@ -166,6 +166,7 @@ rspamd_config_new (void) cfg->ssl_ciphers = "HIGH:!aNULL:!kRSA:!PSK:!SRP:!MD5:!RC4"; cfg->max_message = DEFAULT_MAX_MESSAGE; cfg->monitored_ctx = rspamd_monitored_ctx_init (); + cfg->redis_pool = rspamd_redis_pool_init (); REF_INIT_RETAIN (cfg, rspamd_config_free); diff --git a/src/libserver/monitored.c b/src/libserver/monitored.c index b69261234..ab6922e73 100644 --- a/src/libserver/monitored.c +++ b/src/libserver/monitored.c @@ -65,7 +65,7 @@ struct rspamd_monitored { }; #define msg_err_mon(...) rspamd_default_log_function (G_LOG_LEVEL_CRITICAL, \ - "map", m->tag, \ + "monitored", m->tag, \ G_STRFUNC, \ __VA_ARGS__) #define msg_warn_mon(...) rspamd_default_log_function (G_LOG_LEVEL_WARNING, \ diff --git a/src/libserver/redis_pool.c b/src/libserver/redis_pool.c new file mode 100644 index 000000000..f3f64d2f5 --- /dev/null +++ b/src/libserver/redis_pool.c @@ -0,0 +1,378 @@ +/*- + * Copyright 2016 Vsevolod Stakhov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "config.h" +#include <event.h> +#include "redis_pool.h" +#include "cfg_file.h" +#include "contrib/hiredis/hiredis.h" +#include "contrib/hiredis/async.h" +#include "contrib/hiredis/adapters/libevent.h" +#include "cryptobox.h" +#include "ref.h" +#include "logger.h" + +struct rspamd_redis_pool_elt; + +struct rspamd_redis_pool_connection { + struct redisAsyncContext *ctx; + struct rspamd_redis_pool_elt *elt; + GList *entry; + struct event timeout; + gboolean active; + gchar tag[MEMPOOL_UID_LEN]; + ref_entry_t ref; +}; + +struct rspamd_redis_pool_elt { + struct rspamd_redis_pool *pool; + guint64 key; + GQueue *active; + GQueue *inactive; +}; + +struct rspamd_redis_pool { + struct event_base *ev_base; + struct rspamd_config *cfg; + GHashTable *elts_by_key; + GHashTable *elts_by_ctx; + gdouble timeout; + guint max_conns; +}; + +static const gdouble default_timeout = 60.0; +static const guint default_max_conns = 100; + +#define msg_err_rpool(...) rspamd_default_log_function (G_LOG_LEVEL_CRITICAL, \ + "redis_pool", conn->tag, \ + G_STRFUNC, \ + __VA_ARGS__) +#define msg_warn_rpool(...) rspamd_default_log_function (G_LOG_LEVEL_WARNING, \ + "redis_pool", conn->tag, \ + G_STRFUNC, \ + __VA_ARGS__) +#define msg_info_rpool(...) rspamd_default_log_function (G_LOG_LEVEL_INFO, \ + "redis_pool", conn->tag, \ + G_STRFUNC, \ + __VA_ARGS__) +#define msg_debug_rpool(...) rspamd_default_log_function (G_LOG_LEVEL_DEBUG, \ + "redis_pool", conn->tag, \ + G_STRFUNC, \ + __VA_ARGS__) + +static inline guint64 +rspamd_redis_pool_get_key (const gchar *db, const gchar *password, + const char *ip, int port) +{ + rspamd_cryptobox_fast_hash_state_t st; + + rspamd_cryptobox_fast_hash_init (&st, rspamd_hash_seed ()); + + if (db) { + rspamd_cryptobox_fast_hash_update (&st, db, strlen (db)); + } + if (password) { + rspamd_cryptobox_fast_hash_update (&st, password, strlen (password)); + } + + rspamd_cryptobox_fast_hash_update (&st, ip, strlen (ip)); + rspamd_cryptobox_fast_hash_update (&st, &port, sizeof (port)); + + return rspamd_cryptobox_fast_hash_final (&st); +} + + +static void +rspamd_redis_pool_conn_dtor (struct rspamd_redis_pool_connection *conn) +{ + if (conn->active) { + msg_debug_rpool ("active connection removed"); + + if (conn->ctx) { + g_hash_table_remove (conn->elt->pool->elts_by_ctx, conn->ctx); + redisAsyncFree (conn->ctx); + } + + g_queue_unlink (conn->elt->active, conn->entry); + } + else { + msg_debug_rpool ("inactive connection removed"); + + if (event_get_base (&conn->timeout)) { + event_del (&conn->timeout); + } + + g_queue_unlink (conn->elt->inactive, conn->entry); + } + + + g_list_free (conn->entry); + g_slice_free1 (sizeof (*conn), conn); +} + +static void +rspamd_redis_pool_elt_dtor (gpointer p) +{ + GList *cur; + struct rspamd_redis_pool_elt *elt = p; + struct rspamd_redis_pool_connection *c; + + for (cur = elt->active->head; cur != NULL; cur = g_list_next (cur)) { + c = cur->data; + REF_RELEASE (c); + } + + for (cur = elt->inactive->head; cur != NULL; cur = g_list_next (cur)) { + c = cur->data; + REF_RELEASE (c); + } + + g_queue_free (elt->active); + g_queue_free (elt->inactive); + g_slice_free1 (sizeof (*elt), elt); +} + +static void +rspamd_redis_conn_timeout (gint fd, short what, gpointer p) +{ + struct rspamd_redis_pool_connection *conn = p; + + msg_debug_rpool ("scheduled removal of connection"); + REF_RELEASE (conn); +} + +static void +rspamd_redis_pool_schedule_timeout (struct rspamd_redis_pool_connection *conn) +{ + struct timeval tv; + gdouble real_timeout; + guint active_elts; + + active_elts = g_queue_get_length (conn->elt->active); + + if (active_elts > conn->elt->pool->max_conns) { + real_timeout = conn->elt->pool->timeout / 2.0; + real_timeout = rspamd_time_jitter (real_timeout, real_timeout / 4.0); + } + else { + real_timeout = conn->elt->pool->timeout; + real_timeout = rspamd_time_jitter (real_timeout, real_timeout / 2.0); + } + + msg_debug_rpool ("scheduled connection cleanup in %.1f seconds", + real_timeout); + double_to_tv (real_timeout, &tv); + event_set (&conn->timeout, -1, EV_TIMEOUT, rspamd_redis_conn_timeout, conn); + event_base_set (conn->elt->pool->ev_base, &conn->timeout); + event_add (&conn->timeout, &tv); +} + +static struct rspamd_redis_pool_connection * +rspamd_redis_pool_new_connection (struct rspamd_redis_pool *pool, + struct rspamd_redis_pool_elt *elt, + const char *db, + const char *password, + const char *ip, + gint port) +{ + struct rspamd_redis_pool_connection *conn; + struct redisAsyncContext *ctx; + + ctx = redisAsyncConnect (ip, port); + + if (ctx) { + + if (ctx->err != REDIS_OK) { + redisAsyncFree (ctx); + + return NULL; + } + else { + conn = g_slice_alloc0 (sizeof (*conn)); + conn->entry = g_list_prepend (NULL, conn); + conn->elt = elt; + conn->active = TRUE; + + g_hash_table_insert (elt->pool->elts_by_ctx, ctx, conn); + g_queue_push_head_link (elt->active, conn->entry); + conn->ctx = ctx; + rspamd_random_hex (conn->tag, sizeof (conn->tag)); + REF_INIT_RETAIN (conn, rspamd_redis_pool_conn_dtor); + msg_debug_rpool ("created new connection to %s:%d", ip, port); + + redisLibeventAttach (ctx, pool->ev_base); + + if (password) { + redisAsyncCommand (ctx, NULL, NULL, "AUTH %s", password); + } + if (db) { + redisAsyncCommand (ctx, NULL, NULL, "SELECT %s", db); + } + } + + return conn; + } + + return NULL; +} + +static struct rspamd_redis_pool_elt * +rspamd_redis_pool_new_elt (struct rspamd_redis_pool *pool) +{ + struct rspamd_redis_pool_elt *elt; + + elt = g_slice_alloc0 (sizeof (*elt)); + elt->active = g_queue_new (); + elt->inactive = g_queue_new (); + elt->pool = pool; + + return elt; +} + +struct rspamd_redis_pool * +rspamd_redis_pool_init (void) +{ + struct rspamd_redis_pool *pool; + + pool = g_slice_alloc0 (sizeof (*pool)); + pool->elts_by_key = g_hash_table_new_full (g_int64_hash, g_int64_equal, NULL, + rspamd_redis_pool_elt_dtor); + pool->elts_by_ctx = g_hash_table_new (g_direct_hash, g_direct_equal); + + return pool; +} + +void +rspamd_redis_pool_config (struct rspamd_redis_pool *pool, + struct rspamd_config *cfg, + struct event_base *ev_base) +{ + g_assert (pool != NULL); + + pool->ev_base = ev_base; + pool->cfg = cfg; + pool->timeout = default_timeout; + pool->max_conns = default_max_conns; +} + + +struct redisAsyncContext* +rspamd_redis_pool_connect (struct rspamd_redis_pool *pool, + const gchar *db, const gchar *password, + const char *ip, int port) +{ + guint64 key; + struct rspamd_redis_pool_elt *elt; + GList *conn_entry; + struct rspamd_redis_pool_connection *conn; + + g_assert (pool != NULL); + g_assert (pool->ev_base != NULL); + g_assert (ip != NULL); + + key = rspamd_redis_pool_get_key (db, password, ip, port); + elt = g_hash_table_lookup (pool->elts_by_key, &key); + + if (elt) { + if (g_queue_get_length (elt->inactive) > 0) { + conn_entry = g_queue_pop_head_link (elt->inactive); + conn = conn_entry->data; + + if (event_get_base (&conn->timeout)) { + event_del (&conn->timeout); + } + + conn->active = TRUE; + g_queue_push_tail_link (elt->active, conn_entry); + msg_debug_rpool ("reused existing connection to %s:%d", ip, port); + + } + else { + /* Need to create connection */ + conn = rspamd_redis_pool_new_connection (pool, elt, + db, password, ip, port); + } + } + else { + elt = rspamd_redis_pool_new_elt (pool); + elt->key = key; + g_hash_table_insert (pool->elts_by_key, &elt->key, elt); + + conn = rspamd_redis_pool_new_connection (pool, elt, + db, password, ip, port); + } + + REF_RETAIN (conn); + + return conn->ctx; +} + + +void +rspamd_redis_pool_release_connection (struct rspamd_redis_pool *pool, + struct redisAsyncContext *ctx, gboolean is_fatal) +{ + struct rspamd_redis_pool_connection *conn; + + g_assert (pool != NULL); + g_assert (ctx != NULL); + + conn = g_hash_table_lookup (pool->elts_by_ctx, ctx); + if (conn != NULL) { + REF_RELEASE (conn); + + if (is_fatal || ctx->err == REDIS_ERR_IO || ctx->err == REDIS_ERR_EOF) { + /* We need to terminate connection forcefully */ + msg_debug_rpool ("closed connection forcefully"); + REF_RELEASE (conn); + } + else { + /* Just move it to the inactive queue */ + g_queue_unlink (conn->elt->active, conn->entry); + g_queue_push_head_link (conn->elt->inactive, conn->entry); + conn->active = FALSE; + rspamd_redis_pool_schedule_timeout (conn); + msg_debug_rpool ("mark connection inactive"); + } + } + else { + g_assert_not_reached (); + } +} + + +void +rspamd_redis_pool_destroy (struct rspamd_redis_pool *pool) +{ + struct rspamd_redis_pool_elt *elt; + GHashTableIter it; + gpointer k, v; + + g_assert (pool != NULL); + + g_hash_table_iter_init (&it, pool->elts_by_key); + + while (g_hash_table_iter_next (&it, &k, &v)) { + elt = v; + rspamd_redis_pool_elt_dtor (elt); + g_hash_table_iter_steal (&it); + } + + g_hash_table_unref (pool->elts_by_ctx); + g_hash_table_unref (pool->elts_by_key); + + g_slice_free1 (sizeof (*pool), pool); +} diff --git a/src/libserver/redis_pool.h b/src/libserver/redis_pool.h new file mode 100644 index 000000000..5e5dc0b5d --- /dev/null +++ b/src/libserver/redis_pool.h @@ -0,0 +1,70 @@ +/*- + * Copyright 2016 Vsevolod Stakhov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef SRC_LIBSERVER_REDIS_POOL_H_ +#define SRC_LIBSERVER_REDIS_POOL_H_ + +#include "config.h" + +struct rspamd_redis_pool; +struct rspamd_config; +struct redisAsyncContext; +struct event_base; + +/** + * Creates new redis pool + * @return + */ +struct rspamd_redis_pool *rspamd_redis_pool_init (void); + +/** + * Configure redis pool and binds it to a specific event base + * @param cfg + * @param ev_base + */ +void rspamd_redis_pool_config (struct rspamd_redis_pool *pool, + struct rspamd_config *cfg, + struct event_base *ev_base); + + +/** + * Create or reuse the specific redis connection + * @param pool + * @param db + * @param password + * @param ip + * @param port + * @return + */ +struct redisAsyncContext* rspamd_redis_pool_connect ( + struct rspamd_redis_pool *pool, + const gchar *db, const gchar *password, + const char *ip, int port); + +/** + * Release a connection to the pool + * @param pool + * @param ctx + */ +void rspamd_redis_pool_release_connection (struct rspamd_redis_pool *pool, + struct redisAsyncContext *ctx, gboolean is_fatal); + +/** + * Stops redis pool and destroys it + * @param pool + */ +void rspamd_redis_pool_destroy (struct rspamd_redis_pool *pool); + +#endif /* SRC_LIBSERVER_REDIS_POOL_H_ */ diff --git a/src/lua/lua_redis.c b/src/lua/lua_redis.c index c35d9614b..815192d27 100644 --- a/src/lua/lua_redis.c +++ b/src/lua/lua_redis.c @@ -17,10 +17,8 @@ #include "dns.h" #include "utlist.h" -#ifdef WITH_HIREDIS -#include "hiredis.h" -#include "adapters/libevent.h" -#endif +#include "contrib/hiredis/hiredis.h" +#include "contrib/hiredis/async.h" #define REDIS_DEFAULT_TIMEOUT 1.0 @@ -155,6 +153,7 @@ lua_redis_dtor (struct lua_redis_ctx *ctx) struct lua_redis_userdata *ud; struct lua_redis_specific_userdata *cur, *tmp; gboolean is_connected = FALSE; + struct redisAsyncContext *ac; if (ctx->async) { msg_debug ("desctructing %p", ctx); @@ -168,7 +167,10 @@ lua_redis_dtor (struct lua_redis_ctx *ctx) * still be alive here! */ ctx->ref.refcount = 100500; - redisAsyncFree (ud->ctx); + ac = ud->ctx; + ud->ctx = NULL; + rspamd_redis_pool_release_connection (ud->task->cfg->redis_pool, + ac, FALSE); ctx->ref.refcount = 0; is_connected = TRUE; } @@ -384,8 +386,9 @@ lua_redis_callback (redisAsyncContext *c, gpointer r, gpointer priv) ac = ud->ctx; ud->ctx = NULL; - if (ac != NULL) { - redisAsyncFree (ac); + if (ac) { + rspamd_redis_pool_release_connection (ud->task->cfg->redis_pool, + ac, FALSE); } } @@ -413,7 +416,8 @@ lua_redis_timeout (int fd, short what, gpointer u) * This will call all callbacks pending so the entire context * will be destructed */ - redisAsyncFree (ac); + rspamd_redis_pool_release_connection (sp_ud->c->task->cfg->redis_pool, + ac, TRUE); } REDIS_RELEASE (ctx); } @@ -464,22 +468,6 @@ lua_redis_parse_args (lua_State *L, gint idx, const gchar *cmd, *nargs = top; } -static void -lua_redis_connect_cb (const struct redisAsyncContext *c, int status) -{ - /* - * Workaround to prevent double close: - * https://groups.google.com/forum/#!topic/redis-db/mQm46XkIPOY - */ -#if defined(HIREDIS_MAJOR) && HIREDIS_MAJOR == 0 && HIREDIS_MINOR <= 11 - struct redisAsyncContext *nc = (struct redisAsyncContext *)c; - if (status == REDIS_ERR) { - nc->c.fd = -1; - } -#endif -} - - /*** * @function rspamd_redis.make_request({params}) @@ -662,14 +650,15 @@ lua_redis_make_request (lua_State *L) if (ret) { ud->terminated = 0; ud->timeout = timeout; - ud->ctx = redisAsyncConnect (rspamd_inet_address_to_string (addr->addr), + ud->ctx = rspamd_redis_pool_connect (task->cfg->redis_pool, + dbname, password, + rspamd_inet_address_to_string (addr->addr), rspamd_inet_address_get_port (addr->addr)); if (ud->ctx == NULL || ud->ctx->err) { if (ud->ctx) { msg_err_task_check ("cannot connect to redis: %s", ud->ctx->errstr); - redisAsyncFree (ud->ctx); ud->ctx = NULL; } else { @@ -683,16 +672,6 @@ lua_redis_make_request (lua_State *L) return 2; } - redisAsyncSetConnectCallback (ud->ctx, lua_redis_connect_cb); - redisLibeventAttach (ud->ctx, ud->task->ev_base); - - if (password) { - redisAsyncCommand (ud->ctx, NULL, NULL, "AUTH %s", password); - } - if (dbname) { - redisAsyncCommand (ud->ctx, NULL, NULL, "SELECT %s", dbname); - } - ret = redisAsyncCommandArgv (ud->ctx, lua_redis_callback, sp_ud, @@ -719,7 +698,8 @@ lua_redis_make_request (lua_State *L) } else { msg_info_task_check ("call to redis failed: %s", ud->ctx->errstr); - redisAsyncFree (ud->ctx); + rspamd_redis_pool_release_connection (task->cfg->redis_pool, + ud->ctx, FALSE); ud->ctx = NULL; REDIS_RELEASE (ctx); ret = FALSE; @@ -936,7 +916,9 @@ lua_redis_connect (lua_State *L) if (ret && ctx) { ud->terminated = 0; ud->timeout = timeout; - ud->ctx = redisAsyncConnect (rspamd_inet_address_to_string (addr->addr), + ud->ctx = rspamd_redis_pool_connect (task->cfg->redis_pool, + NULL, NULL, + rspamd_inet_address_to_string (addr->addr), rspamd_inet_address_get_port (addr->addr)); if (ud->ctx == NULL || ud->ctx->err) { @@ -948,8 +930,6 @@ lua_redis_connect (lua_State *L) return 1; } - redisAsyncSetConnectCallback (ud->ctx, lua_redis_connect_cb); - redisLibeventAttach (ud->ctx, ud->task->ev_base); pctx = lua_newuserdata (L, sizeof (ctx)); *pctx = ctx; rspamd_lua_setclass (L, "rspamd{redis}", -1); diff --git a/src/plugins/lua/dmarc.lua b/src/plugins/lua/dmarc.lua index 6c34b1198..fc85c4766 100644 --- a/src/plugins/lua/dmarc.lua +++ b/src/plugins/lua/dmarc.lua @@ -29,11 +29,24 @@ local symbols = { spf_softfail_symbol = 'R_SPF_SOFTFAIL', spf_neutral_symbol = 'R_SPF_NEUTRAL', spf_tempfail_symbol = 'R_SPF_DNSFAIL', + spf_na_symbol = 'R_SPF_NA', dkim_allow_symbol = 'R_DKIM_ALLOW', dkim_deny_symbol = 'R_DKIM_REJECT', dkim_tempfail_symbol = 'R_DKIM_TEMPFAIL', + dkim_na_symbol = 'R_DKIM_NA', } + +local dmarc_symbols = { + allow = 'DMARC_POLICY_ALLOW', + badpolicy = 'DMARC_BAD_POLICY', + dnsfail = 'DMARC_DNSFAIL', + na = 'DMARC_NA', + reject = 'DMARC_POLICY_REJECT', + softfail = 'DMARC_POLICY_SOFTFAIL', + quarantine = 'DMARC_POLICY_QUARANTINE', +} + -- Default port for redis upstreams local redis_params = nil local dmarc_redis_key_prefix = "dmarc_" @@ -42,13 +55,6 @@ local elts_re = rspamd_regexp.create_cached("\\s*\\\\{0,1};\\s*") local dmarc_reporting = false local dmarc_actions = {} -local function maybe_force_action(disposition) - local force_action = dmarc_actions[disposition] - if force_action then - task:set_pre_result(force_action, 'Action set by DMARC') - end -end - local function dmarc_report(task, spf_ok, dkim_ok, disposition) local ip = task:get_from_ip() if not ip:is_valid() then @@ -62,6 +68,12 @@ local function dmarc_report(task, spf_ok, dkim_ok, disposition) end local function dmarc_callback(task) + local function maybe_force_action(disposition) + local force_action = dmarc_actions[disposition] + if force_action then + task:set_pre_result(force_action, 'Action set by DMARC') + end + end local from = task:get_from(2) local dmarc_domain local ip_addr = task:get_ip() @@ -73,7 +85,8 @@ local function dmarc_callback(task) if from and from[1] and from[1]['domain'] and not from[2] then dmarc_domain = rspamd_util.get_tld(from[1]['domain']) else - return + task:insert_result(dmarc_symbols['na'], 1.0, 'No From header') + return maybe_force_action('na') end local function dmarc_report_cb(task, err, data) @@ -90,11 +103,11 @@ local function dmarc_callback(task) local lookup_domain = string.sub(to_resolve, 8) if err and err ~= 'requested record is not found' then - task:insert_result('DMARC_DNSFAIL', 1.0, lookup_domain .. ' : ' .. err) + task:insert_result(dmarc_symbols['dnsfail'], 1.0, lookup_domain .. ' : ' .. err) return maybe_force_action('dnsfail') elseif err == 'requested record is not found' and lookup_domain == dmarc_domain then - task:insert_result('DMARC_NA', 1.0, lookup_domain) + task:insert_result(dmarc_symbols['na'], 1.0, lookup_domain) return maybe_force_action('na') end @@ -109,7 +122,7 @@ local function dmarc_callback(task) return end - task:insert_result('DMARC_NA', 1.0, lookup_domain) + task:insert_result(dmarc_symbols['na'], 1.0, lookup_domain) return maybe_force_action('na') end @@ -213,14 +226,14 @@ local function dmarc_callback(task) return else - task:insert_result('DMARC_NA', 1.0, lookup_domain) + task:insert_result(dmarc_symbols['na'], 1.0, lookup_domain) return maybe_force_action('na') end end local res = 0.5 if failed_policy then - task:insert_result('DMARC_BAD_POLICY', res, lookup_domain .. ' : ' .. failed_policy) + task:insert_result(dmarc_symbols['badpolicy'], res, lookup_domain .. ' : ' .. failed_policy) return maybe_force_action('badpolicy') end @@ -260,24 +273,24 @@ local function dmarc_callback(task) local spf_tmpfail = task:get_symbol(symbols['spf_tempfail_symbol']) local dkim_tmpfail = task:get_symbol(symbols['dkim_tempfail_symbol']) if (spf_tmpfail or dkim_tmpfail) then - task:insert_result('DMARC_DNSFAIL', 1.0, lookup_domain .. ' : ' .. 'SPF/DKIM temp error') + task:insert_result(dmarc_symbols['dnsfail'], 1.0, lookup_domain .. ' : ' .. 'SPF/DKIM temp error') return maybe_force_action('dnsfail') end if quarantine_policy then if not pct or pct == 100 or (math.random(100) <= pct) then - task:insert_result('DMARC_POLICY_QUARANTINE', res, lookup_domain) + task:insert_result(dmarc_symbols['quarantine'], res, lookup_domain) disposition = "quarantine" end elseif strict_policy then if not pct or pct == 100 or (math.random(100) <= pct) then - task:insert_result('DMARC_POLICY_REJECT', res, lookup_domain) + task:insert_result(dmarc_symbols['reject'], res, lookup_domain) disposition = "reject" end else - task:insert_result('DMARC_POLICY_SOFTFAIL', res, lookup_domain) + task:insert_result(dmarc_symbols['softfail'], res, lookup_domain) end else - task:insert_result('DMARC_POLICY_ALLOW', res, lookup_domain) + task:insert_result(dmarc_symbols['allow'], res, lookup_domain) end if rua and redis_params and dmarc_reporting then @@ -315,6 +328,14 @@ if not opts or type(opts) ~= 'table' then return end +if opts['symbols'] then + for k,_ in pairs(dmarc_symbols) do + if opts['symbols'][k] then + dmarc_symbols[k] = opts['symbols'][k] + end + end +end + if opts['reporting'] == true then dmarc_reporting = true end @@ -344,12 +365,16 @@ if spf_opts then check_mopt('spf_allow_symbol', spf_opts, 'symbol_allow') check_mopt('spf_softfail_symbol', spf_opts, 'symbol_softfail') check_mopt('spf_neutral_symbol', spf_opts, 'symbol_neutral') + check_mopt('spf_tempfail_symbol', spf_opts, 'symbol_dnsfail') + check_mopt('spf_na_symbol', spf_opts, 'symbol_na') end local dkim_opts = rspamd_config:get_all_opt('dkim') if dkim_opts then - check_mopt('dkim_deny_symbol', 'symbol_reject') - check_mopt('dkim_allow_symbol', 'symbol_allow') + check_mopt('dkim_deny_symbol', dkim_opts, 'symbol_reject') + check_mopt('dkim_allow_symbol', dkim_opts, 'symbol_allow') + check_mopt('dkim_tempfail_symbol', dkim_opts, 'symbol_tempfail') + check_mopt('dkim_na_symbol', dkim_opts, 'symbol_na') end local id = rspamd_config:register_symbol({ @@ -358,23 +383,33 @@ local id = rspamd_config:register_symbol({ callback = dmarc_callback }) rspamd_config:register_symbol({ - name = 'DMARC_POLICY_ALLOW', + name = dmarc_symbols['allow'], flags = 'nice', parent = id, type = 'virtual' }) rspamd_config:register_symbol({ - name = 'DMARC_POLICY_REJECT', + name = dmarc_symbols['reject'], + parent = id, + type = 'virtual' +}) +rspamd_config:register_symbol({ + name = dmarc_symbols['quarantine'], + parent = id, + type = 'virtual' +}) +rspamd_config:register_symbol({ + name = dmarc_symbols['softfail'], parent = id, type = 'virtual' }) rspamd_config:register_symbol({ - name = 'DMARC_POLICY_QUARANTINE', + name = dmarc_symbols['dnsfail'], parent = id, type = 'virtual' }) rspamd_config:register_symbol({ - name = 'DMARC_POLICY_SOFTFAIL', + name = dmarc_symbols['na'], parent = id, type = 'virtual' }) diff --git a/src/worker.c b/src/worker.c index 362849136..0e16922f3 100644 --- a/src/worker.c +++ b/src/worker.c @@ -589,6 +589,8 @@ start_worker (struct rspamd_worker *worker) ctx->ev_base, ctx->resolver->r); rspamd_monitored_ctx_config (worker->srv->cfg->monitored_ctx, worker->srv->cfg, ctx->ev_base, ctx->resolver->r); + rspamd_redis_pool_config (worker->srv->cfg->redis_pool, + worker->srv->cfg, ctx->ev_base); /* XXX: stupid default */ ctx->keys_cache = rspamd_keypair_cache_new (256); diff --git a/test/functional/cases/150_rspamadm.robot b/test/functional/cases/150_rspamadm.robot new file mode 100644 index 000000000..bebcb0e0e --- /dev/null +++ b/test/functional/cases/150_rspamadm.robot @@ -0,0 +1,9 @@ +*** Settings *** +Library Process + +*** Test Cases *** +Config Test + ${result} = Run Process ${RSPAMADM} configtest + Should Match Regexp ${result.stderr} ^$ + Should Match Regexp ${result.stdout} ^syntax OK$ + Should Be Equal As Integers ${result.rc} 0 |