PNG  IHDRQgAMA a cHRMz&u0`:pQ<bKGDgmIDATxwUﹻ& ^CX(J I@ "% (** BX +*i"]j(IH{~R)[~>h{}gy)I$Ij .I$I$ʊy@}x.: $I$Ii}VZPC)I$IF ^0ʐJ$I$Q^}{"r=OzI$gRZeC.IOvH eKX $IMpxsk.쒷/&r[޳<v| .I~)@$updYRa$I |M.e JaֶpSYR6j>h%IRز if&uJ)M$I vLi=H;7UJ,],X$I1AҒJ$ XY XzI@GNҥRT)E@;]K*Mw;#5_wOn~\ DC&$(A5 RRFkvIR}l!RytRl;~^ǷJj اy뷦BZJr&ӥ8Pjw~vnv X^(I;4R=P[3]J,]ȏ~:3?[ a&e)`e*P[4]T=Cq6R[ ~ޤrXR Հg(t_HZ-Hg M$ãmL5R uk*`%C-E6/%[t X.{8P9Z.vkXŐKjgKZHg(aK9ڦmKjѺm_ \#$5,)-  61eJ,5m| r'= &ڡd%-]J on Xm|{ RҞe $eڧY XYrԮ-a7RK6h>n$5AVڴi*ֆK)mѦtmr1p| q:흺,)Oi*ֺK)ܬ֦K-5r3>0ԔHjJئEZj,%re~/z%jVMڸmrt)3]J,T K֦OvԒgii*bKiNO~%PW0=dii2tJ9Jݕ{7"I P9JKTbu,%r"6RKU}Ij2HKZXJ,妝 XYrP ެ24c%i^IK|.H,%rb:XRl1X4Pe/`x&P8Pj28Mzsx2r\zRPz4J}yP[g=L) .Q[6RjWgp FIH*-`IMRaK9TXcq*I y[jE>cw%gLRԕiFCj-ďa`#e~I j,%r,)?[gp FI˨mnWX#>mʔ XA DZf9,nKҲzIZXJ,L#kiPz4JZF,I,`61%2s $,VOϚ2/UFJfy7K> X+6 STXIeJILzMfKm LRaK9%|4p9LwJI!`NsiazĔ)%- XMq>pk$-$Q2x#N ؎-QR}ᶦHZډ)J,l#i@yn3LN`;nڔ XuX5pF)m|^0(>BHF9(cզEerJI rg7 4I@z0\JIi䵙RR0s;$s6eJ,`n 䂦0a)S)A 1eJ,堌#635RIgpNHuTH_SԕqVe ` &S)>p;S$魁eKIuX`I4춒o}`m$1":PI<[v9^\pTJjriRŭ P{#{R2,`)e-`mgj~1ϣLKam7&U\j/3mJ,`F;M'䱀 .KR#)yhTq;pcK9(q!w?uRR,n.yw*UXj#\]ɱ(qv2=RqfB#iJmmL<]Y͙#$5 uTU7ӦXR+q,`I}qL'`6Kͷ6r,]0S$- [RKR3oiRE|nӦXR.(i:LDLTJjY%o:)6rxzҒqTJjh㞦I.$YR.ʼnGZ\ֿf:%55 I˼!6dKxm4E"mG_ s? .e*?LRfK9%q#uh$)i3ULRfK9yxm܌bj84$i1U^@Wbm4uJ,ҪA>_Ij?1v32[gLRD96oTaR׿N7%L2 NT,`)7&ƝL*꽙yp_$M2#AS,`)7$rkTA29_Iye"|/0t)$n XT2`YJ;6Jx".e<`$) PI$5V4]29SRI>~=@j]lp2`K9Jaai^" Ԋ29ORI%:XV5]JmN9]H;1UC39NI%Xe78t)a;Oi Ҙ>Xt"~G>_mn:%|~ޅ_+]$o)@ǀ{hgN;IK6G&rp)T2i୦KJuv*T=TOSV>(~D>dm,I*Ɛ:R#ۙNI%D>G.n$o;+#RR!.eU˽TRI28t)1LWϚ>IJa3oFbu&:tJ*(F7y0ZR ^p'Ii L24x| XRI%ۄ>S1]Jy[zL$adB7.eh4%%누>WETf+3IR:I3Xה)3אOۦSRO'ٺ)S}"qOr[B7ϙ.edG)^ETR"RtRݜh0}LFVӦDB^k_JDj\=LS(Iv─aTeZ%eUAM-0;~˃@i|l @S4y72>sX-vA}ϛBI!ݎߨWl*)3{'Y|iSlEڻ(5KtSI$Uv02,~ԩ~x;P4ցCrO%tyn425:KMlD ^4JRxSهF_}شJTS6uj+ﷸk$eZO%G*^V2u3EMj3k%)okI]dT)URKDS 7~m@TJR~荪fT"֛L \sM -0T KfJz+nإKr L&j()[E&I ߴ>e FW_kJR|!O:5/2跌3T-'|zX ryp0JS ~^F>-2< `*%ZFP)bSn"L :)+pʷf(pO3TMW$~>@~ū:TAIsV1}S2<%ޟM?@iT ,Eūoz%i~g|`wS(]oȤ8)$ ntu`өe`6yPl IzMI{ʣzʨ )IZ2= ld:5+請M$-ї;U>_gsY$ÁN5WzWfIZ)-yuXIfp~S*IZdt;t>KūKR|$#LcԀ+2\;kJ`]YǔM1B)UbG"IRߊ<xܾӔJ0Z='Y嵤 Leveg)$znV-º^3Ւof#0Tfk^Zs[*I꯳3{)ˬW4Ւ4 OdpbZRS|*I 55#"&-IvT&/윚Ye:i$ 9{LkuRe[I~_\ؠ%>GL$iY8 9ܕ"S`kS.IlC;Ҏ4x&>u_0JLr<J2(^$5L s=MgV ~,Iju> 7r2)^=G$1:3G< `J3~&IR% 6Tx/rIj3O< ʔ&#f_yXJiގNSz; Tx(i8%#4 ~AS+IjerIUrIj362v885+IjAhK__5X%nV%Iͳ-y|7XV2v4fzo_68"S/I-qbf; LkF)KSM$ Ms>K WNV}^`-큧32ŒVؙGdu,^^m%6~Nn&͓3ŒVZMsRpfEW%IwdǀLm[7W&bIRL@Q|)* i ImsIMmKmyV`i$G+R 0tV'!V)֏28vU7͒vHꦼtxꗞT ;S}7Mf+fIRHNZUkUx5SAJㄌ9MqμAIRi|j5)o*^'<$TwI1hEU^c_j?Е$%d`z cyf,XO IJnTgA UXRD }{H}^S,P5V2\Xx`pZ|Yk:$e ~ @nWL.j+ϝYb퇪bZ BVu)u/IJ_ 1[p.p60bC >|X91P:N\!5qUB}5a5ja `ubcVxYt1N0Zzl4]7­gKj]?4ϻ *[bg$)+À*x쳀ogO$~,5 زUS9 lq3+5mgw@np1sso Ӻ=|N6 /g(Wv7U;zωM=wk,0uTg_`_P`uz?2yI!b`kĸSo+Qx%!\οe|އԁKS-s6pu_(ֿ$i++T8=eY; צP+phxWQv*|p1. ά. XRkIQYP,drZ | B%wP|S5`~́@i޾ E;Չaw{o'Q?%iL{u D?N1BD!owPHReFZ* k_-~{E9b-~P`fE{AܶBJAFO wx6Rox5 K5=WwehS8 (JClJ~ p+Fi;ŗo+:bD#g(C"wA^ r.F8L;dzdIHUX݆ϞXg )IFqem%I4dj&ppT{'{HOx( Rk6^C٫O.)3:s(۳(Z?~ٻ89zmT"PLtw䥈5&b<8GZ-Y&K?e8,`I6e(֍xb83 `rzXj)F=l($Ij 2*(F?h(/9ik:I`m#p3MgLaKjc/U#n5S# m(^)=y=đx8ŬI[U]~SцA4p$-F i(R,7Cx;X=cI>{Km\ o(Tv2vx2qiiDJN,Ҏ!1f 5quBj1!8 rDFd(!WQl,gSkL1Bxg''՞^ǘ;pQ P(c_ IRujg(Wz bs#P­rz> k c&nB=q+ؔXn#r5)co*Ũ+G?7< |PQӣ'G`uOd>%Mctz# Ԫڞ&7CaQ~N'-P.W`Oedp03C!IZcIAMPUۀ5J<\u~+{9(FbbyAeBhOSܳ1 bÈT#ŠyDžs,`5}DC-`̞%r&ڙa87QWWp6e7 Rϫ/oY ꇅ Nܶըtc!LA T7V4Jsū I-0Pxz7QNF_iZgúWkG83 0eWr9 X]㾮݁#Jˢ C}0=3ݱtBi]_ &{{[/o[~ \q鯜00٩|cD3=4B_b RYb$óBRsf&lLX#M*C_L܄:gx)WΘsGSbuL rF$9';\4Ɍq'n[%p.Q`u hNb`eCQyQ|l_C>Lb꟟3hSb #xNxSs^ 88|Mz)}:](vbۢamŖ࿥ 0)Q7@0=?^k(*J}3ibkFn HjB׻NO z x}7p 0tfDX.lwgȔhԾŲ }6g E |LkLZteu+=q\Iv0쮑)QٵpH8/2?Σo>Jvppho~f>%bMM}\//":PTc(v9v!gոQ )UfVG+! 35{=x\2+ki,y$~A1iC6#)vC5^>+gǵ@1Hy٪7u;p psϰu/S <aʸGu'tD1ԝI<pg|6j'p:tպhX{o(7v],*}6a_ wXRk,O]Lܳ~Vo45rp"N5k;m{rZbΦ${#)`(Ŵg,;j%6j.pyYT?}-kBDc3qA`NWQū20/^AZW%NQ MI.X#P#,^Ebc&?XR tAV|Y.1!؅⨉ccww>ivl(JT~ u`ٵDm q)+Ri x/x8cyFO!/*!/&,7<.N,YDŽ&ܑQF1Bz)FPʛ?5d 6`kQձ λc؎%582Y&nD_$Je4>a?! ͨ|ȎWZSsv8 j(I&yj Jb5m?HWp=g}G3#|I,5v珿] H~R3@B[☉9Ox~oMy=J;xUVoj bUsl_35t-(ՃɼRB7U!qc+x4H_Qo֮$[GO<4`&č\GOc[.[*Af%mG/ ňM/r W/Nw~B1U3J?P&Y )`ѓZ1p]^l“W#)lWZilUQu`-m|xĐ,_ƪ|9i:_{*(3Gѧ}UoD+>m_?VPۅ15&}2|/pIOʵ> GZ9cmíتmnz)yߐbD >e}:) r|@R5qVSA10C%E_'^8cR7O;6[eKePGϦX7jb}OTGO^jn*媓7nGMC t,k31Rb (vyܴʭ!iTh8~ZYZp(qsRL ?b}cŨʊGO^!rPJO15MJ[c&~Z`"ѓޔH1C&^|Ш|rʼ,AwĴ?b5)tLU)F| &g٣O]oqSUjy(x<Ϳ3 .FSkoYg2 \_#wj{u'rQ>o;%n|F*O_L"e9umDds?.fuuQbIWz |4\0 sb;OvxOSs; G%T4gFRurj(֍ڑb uԖKDu1MK{1^ q; C=6\8FR艇!%\YÔU| 88m)֓NcLve C6z;o&X x59:q61Z(T7>C?gcļxѐ Z oo-08jہ x,`' ҔOcRlf~`jj".Nv+sM_]Zk g( UOPyεx%pUh2(@il0ݽQXxppx-NS( WO+轾 nFߢ3M<;z)FBZjciu/QoF 7R¥ ZFLF~#ȣߨ^<쩡ݛкvџ))ME>ώx4m#!-m!L;vv#~Y[đKmx9.[,UFS CVkZ +ߟrY٧IZd/ioi$%͝ب_ֶX3ܫhNU ZZgk=]=bbJS[wjU()*I =ώ:}-蹞lUj:1}MWm=̛ _ ¾,8{__m{_PVK^n3esw5ӫh#$-q=A̟> ,^I}P^J$qY~Q[ Xq9{#&T.^GVj__RKpn,b=`żY@^՝;z{paVKkQXj/)y TIc&F;FBG7wg ZZDG!x r_tƢ!}i/V=M/#nB8 XxЫ ^@CR<{䤭YCN)eKOSƟa $&g[i3.C6xrOc8TI;o hH6P&L{@q6[ Gzp^71j(l`J}]e6X☉#͕ ׈$AB1Vjh㭦IRsqFBjwQ_7Xk>y"N=MB0 ,C #o6MRc0|$)ف"1!ixY<B9mx `,tA>)5ػQ?jQ?cn>YZe Tisvh# GMމȇp:ԴVuږ8ɼH]C.5C!UV;F`mbBk LTMvPʍϤj?ԯ/Qr1NB`9s"s TYsz &9S%U԰> {<ؿSMxB|H\3@!U| k']$U+> |HHMLޢ?V9iD!-@x TIî%6Z*9X@HMW#?nN ,oe6?tQwڱ.]-y':mW0#!J82qFjH -`ѓ&M0u Uγmxϵ^-_\])@0Rt.8/?ٰCY]x}=sD3ojަЫNuS%U}ԤwHH>ڗjܷ_3gN q7[q2la*ArǓԖ+p8/RGM ]jacd(JhWko6ڎbj]i5Bj3+3!\j1UZLsLTv8HHmup<>gKMJj0@H%,W΃7R) ">c, xixј^ aܖ>H[i.UIHc U1=yW\=S*GR~)AF=`&2h`DzT󑓶J+?W+}C%P:|0H܆}-<;OC[~o.$~i}~HQ TvXΈr=b}$vizL4:ȰT|4~*!oXQR6Lk+#t/g lԁߖ[Jڶ_N$k*". xsxX7jRVbAAʯKҎU3)zSNN _'s?f)6X!%ssAkʱ>qƷb hg %n ~p1REGMHH=BJiy[<5 ǁJҖgKR*倳e~HUy)Ag,K)`Vw6bRR:qL#\rclK/$sh*$ 6덤 KԖc 3Z9=Ɣ=o>X Ώ"1 )a`SJJ6k(<c e{%kϊP+SL'TcMJWRm ŏ"w)qc ef꒵i?b7b('"2r%~HUS1\<(`1Wx9=8HY9m:X18bgD1u ~|H;K-Uep,, C1 RV.MR5άh,tWO8WC$ XRVsQS]3GJ|12 [vM :k#~tH30Rf-HYݺ-`I9%lIDTm\ S{]9gOڒMNCV\G*2JRŨ;Rҏ^ڽ̱mq1Eu?To3I)y^#jJw^Ńj^vvlB_⋌P4x>0$c>K†Aļ9s_VjTt0l#m>E-,,x,-W)سo&96RE XR.6bXw+)GAEvL)͞K4$p=Ũi_ѱOjb HY/+@θH9޼]Nԥ%n{ &zjT? Ty) s^ULlb,PiTf^<À] 62R^V7)S!nllS6~͝V}-=%* ʻ>G DnK<y&>LPy7'r=Hj 9V`[c"*^8HpcO8bnU`4JȪAƋ#1_\ XϘHPRgik(~G~0DAA_2p|J묭a2\NCr]M_0 ^T%e#vD^%xy-n}-E\3aS%yN!r_{ )sAw ڼp1pEAk~v<:`'ӭ^5 ArXOI驻T (dk)_\ PuA*BY]yB"l\ey hH*tbK)3 IKZ򹞋XjN n *n>k]X_d!ryBH ]*R 0(#'7 %es9??ښFC,ՁQPjARJ\Ρw K#jahgw;2$l*) %Xq5!U᢯6Re] |0[__64ch&_}iL8KEgҎ7 M/\`|.p,~`a=BR?xܐrQ8K XR2M8f ?`sgWS%" Ԉ 7R%$ N}?QL1|-эټwIZ%pvL3Hk>,ImgW7{E xPHx73RA @RS CC !\ȟ5IXR^ZxHл$Q[ŝ40 (>+ _C >BRt<,TrT {O/H+˟Pl6 I B)/VC<6a2~(XwV4gnXR ϱ5ǀHٻ?tw똤Eyxp{#WK qG%5],(0ӈH HZ])ג=K1j&G(FbM@)%I` XRg ʔ KZG(vP,<`[ Kn^ SJRsAʠ5xՅF`0&RbV tx:EaUE/{fi2;.IAwW8/tTxAGOoN?G}l L(n`Zv?pB8K_gI+ܗ #i?ޙ.) p$utc ~DžfՈEo3l/)I-U?aԅ^jxArA ΧX}DmZ@QLےbTXGd.^|xKHR{|ΕW_h] IJ`[G9{).y) 0X YA1]qp?p_k+J*Y@HI>^?gt.06Rn ,` ?);p pSF9ZXLBJPWjgQ|&)7! HjQt<| ؅W5 x W HIzYoVMGP Hjn`+\(dNW)F+IrS[|/a`K|ͻ0Hj{R,Q=\ (F}\WR)AgSG`IsnAR=|8$}G(vC$)s FBJ?]_u XRvύ6z ŨG[36-T9HzpW̞ú Xg큽=7CufzI$)ki^qk-) 0H*N` QZkk]/tnnsI^Gu't=7$ Z;{8^jB% IItRQS7[ϭ3 $_OQJ`7!]W"W,)Iy W AJA;KWG`IY{8k$I$^%9.^(`N|LJ%@$I}ֽp=FB*xN=gI?Q{٥4B)mw $Igc~dZ@G9K X?7)aK%݅K$IZ-`IpC U6$I\0>!9k} Xa IIS0H$I H ?1R.Чj:4~Rw@p$IrA*u}WjWFPJ$I➓/6#! LӾ+ X36x8J |+L;v$Io4301R20M I$-E}@,pS^ޟR[/s¹'0H$IKyfŸfVOπFT*a$I>He~VY/3R/)>d$I>28`Cjw,n@FU*9ttf$I~<;=/4RD~@ X-ѕzἱI$: ԍR a@b X{+Qxuq$IЛzo /~3\8ڒ4BN7$IҀj V]n18H$IYFBj3̵̚ja pp $Is/3R Ӻ-Yj+L;.0ŔI$Av? #!5"aʄj}UKmɽH$IjCYs?h$IDl843.v}m7UiI=&=0Lg0$I4: embe` eQbm0u? $IT!Sƍ'-sv)s#C0:XB2a w I$zbww{."pPzO =Ɔ\[ o($Iaw]`E).Kvi:L*#gР7[$IyGPI=@R 4yR~̮´cg I$I/<tPͽ hDgo 94Z^k盇΄8I56^W$I^0̜N?4*H`237}g+hxoq)SJ@p|` $I%>-hO0eO>\ԣNߌZD6R=K ~n($I$y3D>o4b#px2$yڪtzW~a $I~?x'BwwpH$IZݑnC㧄Pc_9sO gwJ=l1:mKB>Ab<4Lp$Ib o1ZQ@85b̍ S'F,Fe,^I$IjEdù{l4 8Ys_s Z8.x m"+{~?q,Z D!I$ϻ'|XhB)=…']M>5 rgotԎ 獽PH$IjIPhh)n#cÔqA'ug5qwU&rF|1E%I$%]!'3AFD/;Ck_`9 v!ٴtPV;x`'*bQa w I$Ix5 FC3D_~A_#O݆DvV?<qw+I$I{=Z8".#RIYyjǪ=fDl9%M,a8$I$Ywi[7ݍFe$s1ՋBVA?`]#!oz4zjLJo8$I$%@3jAa4(o ;p,,dya=F9ً[LSPH$IJYЉ+3> 5"39aZ<ñh!{TpBGkj}Sp $IlvF.F$I z< '\K*qq.f<2Y!S"-\I$IYwčjF$ w9 \ߪB.1v!Ʊ?+r:^!I$BϹB H"B;L'G[ 4U#5>੐)|#o0aڱ$I>}k&1`U#V?YsV x>{t1[I~D&(I$I/{H0fw"q"y%4 IXyE~M3 8XψL}qE$I[> nD?~sf ]o΁ cT6"?'_Ἣ $I>~.f|'!N?⟩0G KkXZE]ޡ;/&?k OۘH$IRۀwXӨ<7@PnS04aӶp.:@\IWQJ6sS%I$e5ڑv`3:x';wq_vpgHyXZ 3gЂ7{{EuԹn±}$I$8t;b|591nءQ"P6O5i }iR̈́%Q̄p!I䮢]O{H$IRϻ9s֧ a=`- aB\X0"+5"C1Hb?߮3x3&gşggl_hZ^,`5?ߎvĸ%̀M!OZC2#0x LJ0 Gw$I$I}<{Eb+y;iI,`ܚF:5ܛA8-O-|8K7s|#Z8a&><a&/VtbtLʌI$I$I$I$I$I$IRjDD%tEXtdate:create2022-05-31T04:40:26+00:00!Î%tEXtdate:modify2022-05-31T04:40:26+00:00|{2IENDB` sh-3ll

HOME


sh-3ll 1.0
DIR:/opt/imunify360/venv/lib/python3.11/site-packages/im360/utils/
Upload File :
Current File : //opt/imunify360/venv/lib/python3.11/site-packages/im360/utils/lazy_init.py
"""This plugin periodically checks set of rules and ipsets,
and recreates it if needed, process block/unblock messages."""

import asyncio
import json
import logging
import os
import time
from contextlib import suppress
from pathlib import Path

from typing import Set

from defence360agent.internals.global_scope import g
from defence360agent.utils import log_error_and_ignore, timeit
from defence360agent.utils.common import DAY, ServiceBase, rate_limit
from im360.contracts.config import Firewall, NetworkInterface, DOS, EnhancedDOS
from im360.contracts.config import Protector as Config
from im360.contracts.config import Webshield
from im360.internals.core import IPSetNoRedirectPort, RuleSet, ip_versions
from im360.internals.core.ipset.libipset import (
    IPSetError,
    IPSetCannotBeSwappedIncompatibleType,
)
from im360.internals.core.firewall import get_firewall
from im360.internals.core.firewall.base import (
    FirewallBatchCommandError,
    FirewallError,
    FirewallTemporaryError,
)
from im360.internals.core.ipset.country import IPSetCountry
from im360.internals.core.ipset.ip import (
    IPSetStatic,
    IPSetStaticRemoteProxy,
)
from im360.internals.core.ipset.port_deny import (
    InputPortBlockingDenyModeIPSet,
    OutputPortBlockingDenyModeIPSet,
)
from im360.subsys.webshield_mode import Mode as WebshieldMode
from defence360agent.utils.validate import IP, IPVersion

from im360.subsys import smtp_blocking

logger = logging.getLogger(__name__)

RULES_CHECK_IN_PROGRESS = Path("/var/imunify360/.rules_check_in_progress")


class VersionState:
    def __init__(self):
        self.transient_error_on_create = False
        self.errors = 0
        self.next_try_time = 0.0
        self.running = True


class RulesChecker(ServiceBase):
    """Periodically checks if rules exist and if not, recreate them."""

    #: delay in secs between two iptables rule/ipsets checks
    RULE_CHECK_INTERVAL = int(
        os.environ.get("IMUNIFY360_RULE_CHECK_INTERVAL", 30)
    )
    CONSECUTIVE_ERROR_LIMIT = 10  # retries until giving up
    ERROR_THRESHOLD = 10  # disabled IPv6 support after this many failures
    RETRY_INTERVAL = 3600  # interval between attempts to enable IPv6
    CAPTURE_CSF_LOCK = True
    CSF_LOCK_TIMEOUT = 300  # seconds
    IPSETS_CHECK_INTERVAL = DAY  # seconds

    HOST_IPSETS = {
        IP.V4: {"i360.ipv4.whitelist.host_ips"},
        IP.V6: {"i360.ipv6.whitelist.host_ips"},
    }

    def __init__(self, loop):
        super().__init__(loop)
        self.ruleset = RuleSet()
        # Prevent rules create/destroy public method calls
        # from overlapping with @recurring_check->create_rules/destroy_rules()
        #
        # Use this lock to operate on rules (add to ipset)
        # while keeping the rules in consistent state (not half created
        # or half destroyed).
        self.lock_rules_create_destroy = Config.RULE_EDIT_LOCK

        self.active_interface_conf = NetworkInterface.get_interface_conf()
        self.versions = {ver: VersionState() for ver in ip_versions.all()}
        self._ipsets_outdated_events = {
            ver: asyncio.Event() for ver in self.versions
        }
        self.outdated_ipsets = {ver: set() for ver in ip_versions.all()}
        self.ipset_lock = asyncio.Lock()

    async def recreate_rules_if_needed(self, recreate_any_way=False) -> None:
        # re-reading config file
        async with self.lock_rules_create_destroy:
            new_conf = NetworkInterface.get_interface_conf()
            if not new_conf == self.active_interface_conf:
                logger.info(
                    "Target & ignore interfaces config was changed,"
                    "recreating rules"
                )
                await self._destroy_rules_and_sets(self.active_interface_conf)
            else:
                for version, state in self.versions.items():
                    if self.versions[version].transient_error_on_create:
                        await self.__check_ipset_consistent(version, state)
            await self._ensure_rules_exist_for_all_versions(
                new_conf, recreate_any_way
            )
            self.active_interface_conf = new_conf

    @rate_limit(period=IPSETS_CHECK_INTERVAL)
    @log_error_and_ignore()
    async def check_ipsets_consistent(self):  # pragma: no cover
        await self._check_ipsets_consistent()

    async def _check_ipsets_consistent(self):
        async with self.lock_rules_create_destroy:
            for version, state in self.versions.items():
                await self.__check_ipset_consistent(version, state)

    async def __check_ipset_consistent(
        self, version: int, state: VersionState
    ) -> None:
        with timeit(f"Checking outdated ipsets {version}", log=logger.info):
            if state.running:
                self.outdated_ipsets[version] = self.HOST_IPSETS[version]

                if outdated := await self.ruleset.get_outdated_ipsets(version):
                    outdated_ipsets = set(
                        ip_count.name for ip_count in outdated
                    )
                    self.outdated_ipsets[version].update(outdated_ipsets)

                    logger.warning(
                        f"{outdated} ipsets are outdated. Recreate them"
                    )
                else:
                    logger.info(
                        "Only host ipsets"
                        f" {', '.join(self.HOST_IPSETS[version])} will"
                        " be updated"
                    )
                self._ipsets_outdated_events[version].set()

    async def _ensure_rules(
        self, interface_conf, recreate_any_way=False
    ) -> bool:
        now = time.monotonic()
        target_versions = set(
            version
            for version in ip_versions.all()
            if (
                ip_versions.is_enabled(version)
                or self.versions[version].next_try_time < now
            )
        )
        recreated_any = False
        has_failed = False
        for version in target_versions:
            self.versions[version].running = False
            try:
                async with self.ipset_lock:
                    r = await self._ensure_for(
                        version, interface_conf, recreate_any_way
                    )
                recreated_any = recreated_any or r
            except (FirewallTemporaryError, IPSetError) as exc:
                has_failed = True
                if isinstance(exc, IPSetError):
                    self.versions[version].transient_error_on_create = True
                logger.info(
                    "Transient error while creating firewall rules for %s: %s",
                    version,
                    exc,
                )
            except FirewallError as exc:
                enabled_ip_version = ip_versions.is_enabled(version)
                if enabled_ip_version:
                    has_failed = True
                    self.versions[version].errors += 1
                    if self.versions[version].errors >= self.ERROR_THRESHOLD:
                        if ip_versions.disable(version):
                            self.versions[version].next_try_time = (
                                now + self.RETRY_INTERVAL
                            )
                            logger.info(
                                "%s firewall support is disabled due"
                                " to multiple consecutive errors",
                                version,
                            )
                else:
                    self.versions[version].next_try_time = (
                        now + self.RETRY_INTERVAL
                    )
                logger.warning(
                    "Failed to recreate firewall rules for %s %s: %s",
                    "enabled" if enabled_ip_version else "disabled",
                    version,
                    exc,
                )
            else:
                self.versions[version].running = True
                self.versions[version].errors = 0
                self.versions[version].transient_error_on_create = False
                if not ip_versions.is_enabled(version):
                    ip_versions.enable(version)
                    logger.info("%s firewall support is enabled", version)
        # whether recreated rules for any ip version and no failures
        # in creating rules for enabled ip versions
        return recreated_any and not has_failed

    async def _ensure_rules_exist_for_all_versions(
        self, interface_conf=None, recreate_any_way=False
    ):
        interface_conf = interface_conf or self.active_interface_conf
        if await self._ensure_rules(interface_conf, recreate_any_way):
            logger.info(
                "Rules and sets successfully recreated for enabled ip versions"
            )

    async def _recreate_for(
        self,
        ip_version: IPVersion,
        interface_conf: dict,
        recreate_ipsets: bool,
        outdated_ipsets: Set[str],
        missing_ipsets: Set[str],
        redundant_ipsets: Set[str],
        recreate_rules: bool,
    ):
        we_have_ipsets_to_destroy = self.ruleset.has_ipset_to_destroy(
            ip_version, redundant_ipsets
        )
        if recreate_rules or we_have_ipsets_to_destroy:
            with timeit(f"Destroying rules for {ip_version}", log=logger.info):
                await self._destroy_rules(interface_conf, ip_version)

        if redundant_ipsets:
            with timeit(
                f"Destroying redundant ipsets: {redundant_ipsets}",
                log=logger.info,
            ):
                await self.ruleset.destroy_ipsets(ip_version, redundant_ipsets)
        else:
            self.ruleset.clean_previously_failed_ipsets(ip_version)

        if missing_ipsets:
            with timeit(
                f"Creating missing ipsets: {missing_ipsets}", log=logger.info
            ):
                await self.ruleset.fill_ipsets(ip_version, missing_ipsets)

        if recreate_rules or we_have_ipsets_to_destroy:
            with timeit(f"Recreating rules for {ip_version}", log=logger.info):
                await self._create_rules(interface_conf, ip_version)

        if recreate_ipsets:
            with timeit("Recreating ipsets", log=logger.info):
                try:
                    await self.ruleset.recreate_ipsets(
                        ip_version, outdated_ipsets
                    )
                except IPSetCannotBeSwappedIncompatibleType:
                    logger.warning(
                        "Outdated ipset has incompatible type, so it cannot be"
                        " swapped. Ipset needs to be recreated after"
                        " destroying rules."
                    )
                    await self._destroy_rules(interface_conf, ip_version)
                    await self.ruleset.destroy_ipsets(
                        ip_version, outdated_ipsets
                    )
                    await self.ruleset.fill_ipsets(ip_version, outdated_ipsets)
                    await self._create_rules(interface_conf, ip_version)

    async def _ensure_for(
        self,
        ip_version: IPVersion,
        interface_conf: dict,
        recreate_any_way=False,
    ) -> bool:
        """Creates imunify360 ruleset for given IP version in iptables.

        If all required ipsets, rules and chains exist, does nothing.
        Otherwise recreates everything as required.

        Returns True if rules or sets has been (re-)created, False
        otherwise."""
        existing_ipsets = await self.ruleset.existing_ipsets(ip_version)
        required_ipsets = self.ruleset.required_ipsets(ip_version)

        missing_ipsets = required_ipsets.copy()
        to_refill_ipsets = self.ruleset.ipsets_to_refill(
            ip_version, existing_ipsets, required_ipsets
        )
        ipsets_ok = (
            existing_ipsets == required_ipsets
            and not self._ipsets_outdated_events[ip_version].is_set()
            and not to_refill_ipsets
        )
        redundant_ipsets = existing_ipsets - required_ipsets
        missing_ipsets -= existing_ipsets
        # don't log in case when no existing ipsets, it is OK on start
        if existing_ipsets and existing_ipsets != required_ipsets:
            _log_ipsets_mismatch(
                missing_ipsets=missing_ipsets,
                redundant_ipsets=redundant_ipsets,
                log=logger.warning,
            )
        rules_ok = await self._rules_ok(interface_conf, ip_version)
        # remove redundant ipsets from outdated ipsets, as they will be
        # removed anyway
        # remove missing ipsets from outdated ipsets, as they will be
        # created anyway
        outdated_ipsets = (
            self.outdated_ipsets[ip_version]
            - redundant_ipsets
            - missing_ipsets
        ) | to_refill_ipsets

        if g.get("DEBUG"):
            logger.info("Required ipsets: %s", required_ipsets)
            logger.info("Existing ipsets: %s", existing_ipsets)
            logger.info("Missing ipsets: %s", missing_ipsets)
            logger.info("Redundant ipsets: %s", redundant_ipsets)
            logger.info("Outdated ipsets: %s", outdated_ipsets)
            logger.info(
                "dbg Rules status for %s [rules: %s], [ipset: %s],"
                " [forced: %s]",
                ip_version,
                "ok" if rules_ok else "bad",
                "ok" if ipsets_ok else "bad",
                recreate_any_way,
            )

        if not (rules_ok and ipsets_ok and not recreate_any_way):
            if not ipsets_ok:  # ipsets will be re-created
                self._ipsets_outdated_events[ip_version].clear()
                self.outdated_ipsets[ip_version].clear()
            logger.info(
                "Rules status for %s [rules: %s], [ipset: %s], [forced: %s]",
                ip_version,
                "ok" if rules_ok else "bad",
                "ok" if ipsets_ok else "bad",
                recreate_any_way,
            )
            await self._recreate_for(
                ip_version,
                interface_conf,
                recreate_ipsets=not ipsets_ok,
                outdated_ipsets=outdated_ipsets,
                redundant_ipsets=redundant_ipsets,
                missing_ipsets=missing_ipsets,
                recreate_rules=not rules_ok or recreate_any_way,
            )
            return True
        return False

    async def _create_rules(
        self, interface_conf, ip_version: IPVersion
    ) -> None:
        logger.info("Creating rules for %s", ip_version)
        async with await get_firewall(ip_version) as firewall:
            batch = await self.ruleset.create_commands(
                firewall, interface_conf, ip_version
            )
            await firewall.commit(batch)

    async def _destroy_rules(
        self, interface_conf, ip_version: IPVersion
    ) -> None:
        logger.info("Destroying rules for %s", ip_version)
        async with await get_firewall(ip_version) as firewall:
            for batch in self.ruleset.destroy_commands(
                firewall, interface_conf, ip_version
            ):
                with suppress(FirewallBatchCommandError):
                    # Command may fail and that is ok
                    await firewall.commit(batch)

    async def _rules_ok(self, interface_conf, ip_version: IPVersion) -> bool:
        async with await get_firewall(ip_version) as firewall:
            actions = await self.ruleset.check_commands(
                firewall, interface_conf, ip_version
            )
            try:
                await firewall.commit(actions)
            except FirewallBatchCommandError:
                return False
            else:
                return True

    async def _destroy_rules_and_sets(
        self,
        interface_conf,
        destroy_rules=True,
        destroy_ipsets=True,
        force_destroy_ipset=False,
    ):
        errors = []
        for ip_version in ip_versions.enabled():
            self.versions[ip_version].running = False
            try:
                if destroy_rules:
                    await self._destroy_rules(interface_conf, ip_version)
                if destroy_ipsets:
                    await self.ruleset.destroy_ipsets(
                        ip_version, force=force_destroy_ipset
                    )
            except Exception as e:
                errors.append(e)

        if not errors:
            return
        elif len(errors) == 1:
            raise errors[0]
        elif len(errors) == 2:
            # hack: to get "free" readable traceback for both errors
            raise errors[1] from errors[0]
        else:  # pragma: no cover
            assert 0, "max 2 ip versions expected"

    async def clear_everything(self, interface_conf=None) -> None:
        interface_conf = interface_conf or self.active_interface_conf
        async with self.lock_rules_create_destroy:
            await smtp_blocking.reset_rules_for_all_versions()
            await self._destroy_rules_and_sets(
                interface_conf, force_destroy_ipset=True
            )

    async def clear_rules(self, interface_conf=None) -> None:
        interface_conf = interface_conf or self.active_interface_conf
        async with self.lock_rules_create_destroy:
            await smtp_blocking.reset_rules_for_all_versions()
            await self._destroy_rules_and_sets(
                interface_conf, destroy_ipsets=False
            )

    async def check_smtp_state_and_reset(self, check_settings=False):
        new_settings = smtp_blocking.read_SMTP_settings()
        if check_settings and not self._is_smtp_settings_changed(new_settings):
            return

        if not await smtp_blocking.is_SMTP_blocking_supported():
            return

        if await smtp_blocking.conflicts_exist():
            await smtp_blocking.reset_rules_for_all_versions()
            return

        await smtp_blocking.sync_rules_for_all_versions(new_settings)

    @staticmethod
    def _is_smtp_settings_changed(new_settings):
        return any(
            active_settings != new_settings
            for active_settings in smtp_blocking.get_active_settings_list()
        )


class RealProtector:
    _rules_checker: RulesChecker

    def __init__(self):
        self._port_blocking_mode = Firewall.port_blocking_mode
        self._dos_enabled: bool = DOS.ENABLED or EnhancedDOS.ENABLED
        self._port_blocking_deny_mode_values = (
            self._get_port_blocking_deny_mode_values()
        )
        # saving webshield status before updating the rules
        # (on _rules_checker.start()), to avoid re-checking
        # the rules on the 1st ConfigUpdate (on start-up)
        self._webshield_status = (
            Webshield.ENABLE,
            WebshieldMode.wants_redirect(WebshieldMode.get()),
            Webshield.SPLASH_SCREEN,
            Webshield.PANEL_PROTECTION,
        )

    def _get_port_blocking_deny_mode_values(self):
        return (
            Firewall.TCP_IN_IPV4,
            Firewall.TCP_OUT_IPV4,
            Firewall.UDP_IN_IPV4,
            Firewall.UDP_OUT_IPV4,
            Firewall.TCP_IN_IPV6,
            Firewall.TCP_OUT_IPV6,
            Firewall.UDP_IN_IPV6,
            Firewall.UDP_OUT_IPV6,
        )

    async def process_global_whitelist_update(self):
        # whitelist update will be applied to ipset when rules/ipsets
        # are reloaded back from the db
        async with self._rules_checker.lock_rules_create_destroy:
            logger.info("Applying global white list update")
            for ipset in [IPSetStatic(), IPSetStaticRemoteProxy()]:
                if ipset.is_enabled():
                    await ipset.reset()

    async def process_country_list_update(self) -> None:
        async with self._rules_checker.lock_rules_create_destroy:
            logger.info("Updating ipset rules on geo ip update")
            for ip_version in ip_versions.enabled():
                await IPSetCountry().restore(ip_version)

    async def _on_config_update_unlocked(self, message):
        recreate = False
        current_status = (
            Webshield.ENABLE,
            WebshieldMode.wants_redirect(WebshieldMode.get()),
            Webshield.SPLASH_SCREEN,
            Webshield.PANEL_PROTECTION,
        )
        if current_status != self._webshield_status:
            logger.info(
                "Webshield status (Webshield.ENABLE, "
                "WebshieldMode.wants_redirect, Webshield.SPLASH_SCREEN, "
                "Webshield.PANEL_PROTECTION) changed from %s to %s",
                self._webshield_status,
                current_status,
            )
            self._webshield_status = current_status
            recreate = True
        current_port_blocking_mode = Firewall.port_blocking_mode
        if self._port_blocking_mode != current_port_blocking_mode:
            logger.info(
                "Ports blocking mode changed from %s to %s",
                self._port_blocking_mode,
                current_port_blocking_mode,
            )
            self._port_blocking_mode = current_port_blocking_mode
            recreate = True
        current_dos_enabled = DOS.ENABLED or EnhancedDOS.ENABLED
        if self._dos_enabled != current_dos_enabled:
            logger.info(
                "Effective DoS protection status changed "
                "from %s to %s. Triggering rules recreation.",
                self._dos_enabled,
                current_dos_enabled,
            )
            self._dos_enabled = current_dos_enabled
            recreate = True
        refill_bp_ipest = self._port_blocking_mode == "DENY"
        if recreate:  # recreate everything
            await self._rules_checker.recreate_rules_if_needed(
                recreate_any_way=True
            )
            logger.info("Firewall rules recreated due to ConfigUpdate")
            if refill_bp_ipest and await self._refill_port_blocking_ipsets():
                logger.info("Blocked ports ipsets reffiled")
        elif (
            await self._update_port_blocking_deny_mode_ipsets_if_needed()
        ):  # update just port blocking deny mode
            logger.info("Blocked ports deny mode updated on ConfigUpdate")

    async def _update_port_blocking_deny_mode_ipsets_if_needed(self):
        updated = False
        new_ports_blocking_values = self._get_port_blocking_deny_mode_values()
        if new_ports_blocking_values != self._port_blocking_deny_mode_values:
            logger.info(
                "Port blocking deny mode changed from %s to %s",
                _format_ports(self._port_blocking_deny_mode_values),
                _format_ports(new_ports_blocking_values),
            )
            self._port_blocking_deny_mode_values = new_ports_blocking_values
            updated = await self._refill_port_blocking_ipsets()

        return updated

    async def _refill_port_blocking_ipsets(self):
        updated = True
        async with self._rules_checker.lock_rules_create_destroy:
            sets = [
                InputPortBlockingDenyModeIPSet(),
                OutputPortBlockingDenyModeIPSet(),
                IPSetNoRedirectPort(),
            ]
            for ip_set in sets:
                try:
                    await ip_set.restore(IP.V4)
                    await ip_set.restore(IP.V6)
                except IPSetError as e:
                    logger.error(
                        "Failed to update ipset %s: %s",
                        ip_set.__class__.__name__,
                        e,
                    )
                    updated = False
                    continue

        return updated


def _log_ipsets_mismatch(missing_ipsets, redundant_ipsets, log):
    """Report missing/redundant ipsets."""
    assert missing_ipsets or redundant_ipsets
    log(
        "Detected %s%s%s ipsets while ensuring ipsets/rules%s%s",
        "missing" * bool(missing_ipsets),
        "/" * bool(missing_ipsets and redundant_ipsets),
        "redundant" * bool(redundant_ipsets),
        f"; missing ipsets: {missing_ipsets}" * bool(missing_ipsets),
        f"; redundant ipsets: {redundant_ipsets}" * bool(redundant_ipsets),
    )


def _format_ports(ports):
    """Format ports for logging."""
    # note: the order is asserted in unit tests
    names = (
        "TCP_IN_IPV4",
        "TCP_OUT_IPV4",
        "UDP_IN_IPV4",
        "UDP_OUT_IPV4",
        "TCP_IN_IPV6",
        "TCP_OUT_IPV6",
        "UDP_IN_IPV6",
        "UDP_OUT_IPV6",
    )
    return json.dumps(dict(zip(names, ports)))