Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
095aeda840 | ||
|
|
3fb02527a4 | ||
|
|
25a61ce632 | ||
|
|
21d94ff178 | ||
|
|
196c55e931 | ||
|
|
0fa76ef75a | ||
|
|
a147a8b0e1 | ||
|
|
1bcb62e31c | ||
|
|
a57643404c | ||
|
|
80805cabfc | ||
|
|
4fe4ac8dde | ||
|
|
0b3ad780fe | ||
|
|
ebf5784b8c | ||
|
|
3b02098817 | ||
|
|
96661f25ab | ||
|
|
7678f59827 | ||
|
|
4b54a07d71 | ||
|
|
e74baae666 | ||
|
|
cb042dda7d | ||
|
|
3245b8018b | ||
|
|
1cd3118da3 | ||
|
|
98c7dd04a4 | ||
|
|
d91d949e95 | ||
|
|
1e8ce20b32 | ||
|
|
8e3a26bdeb | ||
|
|
ba839af8ad | ||
|
|
f8f59dd31a | ||
|
|
5ee482bc9a | ||
|
|
d086317cc8 | ||
|
|
17eed32e03 | ||
|
|
621d8cae17 | ||
|
|
ec043cd202 | ||
|
|
d7afcde2e1 | ||
|
|
1929e7a76b | ||
|
|
320a3dcdd5 | ||
|
|
a98cc08b31 | ||
|
|
5093ff8956 | ||
|
|
d6b884f3b7 | ||
|
|
5ac303df8a | ||
|
|
8d45deca86 | ||
|
|
974ba688d4 | ||
|
|
7f22c4873a | ||
|
|
1414a44b8e | ||
|
|
ad1f9dc4d0 | ||
|
|
64217fe018 | ||
|
|
c0f9348db5 | ||
|
|
d709ec8179 | ||
|
|
ba282b794c | ||
|
|
e323c148e1 | ||
|
|
1e4a14fb25 | ||
|
|
2f99d5accb | ||
|
|
cb667edf17 | ||
|
|
ade79635b2 | ||
|
|
39fe0bdce6 | ||
|
|
10cd815525 | ||
|
|
c03fe6682c | ||
|
|
e3f2fcfab4 | ||
|
|
2ed414ed09 | ||
|
|
410a6ce5c8 | ||
|
|
579c6a94af | ||
|
|
43f2923fbd | ||
|
|
bbfff18280 | ||
|
|
c7a86b4616 | ||
|
|
2a508da5a0 | ||
|
|
38535b0cca | ||
|
|
d2f5c7de09 | ||
|
|
92f90038fa | ||
|
|
8b984a0135 | ||
|
|
6be1a5a0b1 | ||
|
|
8afea37494 | ||
|
|
c5f1598a21 | ||
|
|
ae3e9b9149 | ||
|
|
87f717e0e0 | ||
|
|
3d52fe3e4e | ||
|
|
0a05dbdbfc | ||
|
|
3312b8cb95 | ||
|
|
1627d4f573 | ||
|
|
f07e0f7ade | ||
|
|
ec4e0dd65b | ||
|
|
2367ad115c | ||
|
|
5e3de882af | ||
|
|
3f9cf27853 | ||
|
|
4f8427178a | ||
|
|
6ac2156907 | ||
|
|
3e1aa03037 | ||
|
|
ef5c826c74 | ||
|
|
2918849f9e | ||
|
|
88f779ed24 | ||
|
|
5a8f1bceb8 | ||
|
|
2958ad8636 | ||
|
|
8f6a8df3a4 | ||
|
|
5debaa4cac | ||
|
|
e4e7a83cff | ||
|
|
71ce548117 | ||
|
|
b5b615731e | ||
|
|
319cfefac2 | ||
|
|
d3f5d2d52b | ||
|
|
f0214b3d36 | ||
|
|
ab0847e2d5 | ||
|
|
b6fac90b5a | ||
|
|
63b3f39adc | ||
|
|
8b741129a5 | ||
|
|
7d173b7ce0 | ||
|
|
6893d5a8b7 | ||
|
|
d74dd9321b | ||
|
|
b2f6a3a658 | ||
|
|
ff09c52927 | ||
|
|
dda70d60c0 | ||
|
|
c5d43d7573 | ||
|
|
7579072e3b | ||
|
|
4eba9f2a4b | ||
|
|
2d51240b3e | ||
|
|
977dfe4bd7 | ||
|
|
14edeed310 | ||
|
|
0c4d356d11 | ||
|
|
080dd12288 | ||
|
|
a838deba7f | ||
|
|
1215cb28ac | ||
|
|
d445b5671d | ||
|
|
7f38b80d35 | ||
|
|
9eaa2322b0 | ||
|
|
5aa20c30d8 | ||
|
|
5d8f500471 | ||
|
|
2686811593 | ||
|
|
80a33b1dee | ||
|
|
b259e81911 | ||
|
|
d8e450cf12 | ||
|
|
4b17dddc23 | ||
|
|
ccf4f3a07d | ||
|
|
9659033523 | ||
|
|
cbac7c176a | ||
|
|
6cf4669dee | ||
|
|
d135aeea16 | ||
|
|
8757563be1 | ||
|
|
996920cdaa | ||
|
|
061fab898a | ||
|
|
e587526ede | ||
|
|
343ccd7e8a | ||
|
|
9bdc4be6cc | ||
|
|
725354c793 | ||
|
|
529866b2bb | ||
|
|
f15ed35619 | ||
|
|
08393f9d82 | ||
|
|
df5e024fb8 | ||
|
|
fb08534ed7 | ||
|
|
3eaefe8da0 | ||
|
|
04aba6168c | ||
|
|
a45b1c974c | ||
|
|
63acc862b1 | ||
|
|
fb6315ea5e | ||
|
|
6f4b6ec8cc | ||
|
|
e3d43d1241 | ||
|
|
92a2242174 | ||
|
|
e353fb8024 | ||
|
|
55d7d9e214 | ||
|
|
884cbbd6e1 | ||
|
|
abc5a4e062 | ||
|
|
a0352182e8 | ||
|
|
294ec73f62 | ||
|
|
9f1d1f4fbd | ||
|
|
42dee38ad2 | ||
|
|
2b6bb2c1be | ||
|
|
8ae9208454 | ||
|
|
a2f0dbad87 | ||
|
|
f4f92aa72d | ||
|
|
d678440658 | ||
|
|
ea26509594 | ||
|
|
6198b4566c | ||
|
|
366c64cb9d | ||
|
|
409348394e | ||
|
|
7f185a828e | ||
|
|
69c96c00c0 | ||
|
|
dc4ee5aa47 | ||
|
|
c75748c5d3 | ||
|
|
3700a9e523 | ||
|
|
21dd317870 | ||
|
|
dbc77c47c3 | ||
|
|
913c37db80 | ||
|
|
0f44a3408a | ||
|
|
164105fab1 | ||
|
|
5f3f4d1e2f | ||
|
|
9df332fdef | ||
|
|
2781e7b9d1 | ||
|
|
2d4c3edeb6 | ||
|
|
c9907bef0d | ||
|
|
097d9ea7fe | ||
|
|
3c8aa0e6ba | ||
|
|
0a7f5b7b5c | ||
|
|
e60b10392d | ||
|
|
f259dac747 | ||
|
|
747d468322 | ||
|
|
2c60d08b28 | ||
|
|
fe334590cd | ||
|
|
73b39e0894 | ||
|
|
6b5cc2dee8 | ||
|
|
ba7daef86c | ||
|
|
3bb19ecc9d | ||
|
|
7d256681a4 | ||
|
|
567039f261 | ||
|
|
27e435a7f5 | ||
|
|
cecc3d62ff | ||
|
|
d0ba27cf46 | ||
|
|
d959f6d199 | ||
|
|
c0256724a7 | ||
|
|
27c40a77a3 | ||
|
|
b8e8a87ee9 | ||
|
|
0f27a8add8 | ||
|
|
a03109d096 | ||
|
|
eea1881b73 | ||
|
|
b78d1e439a | ||
|
|
753fff9c15 | ||
|
|
071048b8f2 | ||
|
|
d04c6c13ed | ||
|
|
1cefb7fdc6 | ||
|
|
f5ea40c7da | ||
|
|
adbfdc222d | ||
|
|
ca4da9d1d3 | ||
|
|
9f635c66fb | ||
|
|
66ff90408f | ||
|
|
690ad20958 | ||
|
|
6ef69b48ca | ||
|
|
9d77fb2b4c | ||
|
|
960e30cfcd | ||
|
|
d253e60a8b | ||
|
|
74b73bce8a | ||
|
|
70c601e9e5 | ||
|
|
4d2734dd86 | ||
|
|
8bc7623388 | ||
|
|
be3115cda0 | ||
|
|
b2036c1d62 | ||
|
|
e9071b642a | ||
|
|
41b88b4f4f | ||
|
|
1e3da21c68 | ||
|
|
57caf8ec90 | ||
|
|
7327fee2c8 | ||
|
|
4bc8bc9d5f | ||
|
|
2216fd8da4 | ||
|
|
1ea36c3045 | ||
|
|
9d076af9a2 | ||
|
|
98983adf76 | ||
|
|
067b76fc9e | ||
|
|
fd25c4cbb8 | ||
|
|
56fefe9240 | ||
|
|
a5ab4406d8 | ||
|
|
ec3000798d | ||
|
|
dd8081ade5 | ||
|
|
d7ef5d166d | ||
|
|
78fc99ec7e | ||
|
|
1da9ab78be | ||
|
|
3c83c19070 | ||
|
|
412120ac3c | ||
|
|
6cff2bb007 | ||
|
|
1588200274 | ||
|
|
43f699b872 | ||
|
|
ede2009708 | ||
|
|
343ec1cdd2 | ||
|
|
38b32a9a72 | ||
|
|
6e570b8644 | ||
|
|
5b16973f08 | ||
|
|
9905904bba | ||
|
|
00c5d33e45 | ||
|
|
4c22d4a801 | ||
|
|
d42ec210cc | ||
|
|
8b43d54878 | ||
|
|
32f02ef3b3 | ||
|
|
2ddf687c31 | ||
|
|
f088e7bf76 | ||
|
|
ef1eaf9b37 | ||
|
|
c15c300d2a | ||
|
|
e6ca04ca9d | ||
|
|
35591db3e8 | ||
|
|
7282cf91b0 | ||
|
|
65ce64ba6d | ||
|
|
01333fc3e2 | ||
|
|
303861d7c7 | ||
|
|
ccbc0c7405 | ||
|
|
a7c68ea19f | ||
|
|
cd42cb26d6 | ||
|
|
3befbb1770 | ||
|
|
7f1b578af4 | ||
|
|
8cbc1e0ebb | ||
|
|
546fff8dc0 | ||
|
|
d2d3762ba9 | ||
|
|
5fa1f69264 | ||
|
|
3965d690b1 | ||
|
|
50fbcda8b6 | ||
|
|
ed4b5b8bdc | ||
|
|
94b64055e1 | ||
|
|
9f701bb611 | ||
|
|
3f5c382a98 | ||
|
|
bb0f732ae6 | ||
|
|
8694c54728 | ||
|
|
70e47c9621 | ||
|
|
d004a2c79b | ||
|
|
27fb4790fb | ||
|
|
4860d69909 | ||
|
|
ac41b8f3be | ||
|
|
587e4ff573 | ||
|
|
5dd85bf4f8 | ||
|
|
b2e8d610a0 | ||
|
|
1bcb39c699 | ||
|
|
24d1ea9aa2 | ||
|
|
3515d06979 | ||
|
|
0824ad0d73 | ||
|
|
8c6a547215 | ||
|
|
f51712d275 | ||
|
|
990c410877 | ||
|
|
822f8adb6a | ||
|
|
6b7825b8c8 | ||
|
|
15734a923b | ||
|
|
1cdd618232 | ||
|
|
5d5253dd70 | ||
|
|
581f13c2db | ||
|
|
0a209f01c2 | ||
|
|
af611db859 | ||
|
|
439f97fc52 | ||
|
|
80f97eaf02 | ||
|
|
5045c4bb00 | ||
|
|
2e7a600851 | ||
|
|
686a8bdc54 | ||
|
|
03b2b79c41 | ||
|
|
0ed3a63151 | ||
|
|
3d4044084a | ||
|
|
759f7274f8 | ||
|
|
ca49f2c71c | ||
|
|
c024c93ec6 | ||
|
|
7c7dfab8db | ||
|
|
f48677db7d | ||
|
|
5137cd2ec8 | ||
|
|
939f0d43e0 | ||
|
|
f69a8693fc | ||
|
|
ca85ddc866 | ||
|
|
74eac6c94f | ||
|
|
633b179b19 | ||
|
|
c47bb523b7 | ||
|
|
f0c5bd56f4 | ||
|
|
337be4b36c | ||
|
|
1e59d53bbe | ||
|
|
17a4f81e23 | ||
|
|
7f0bba94ba | ||
|
|
4e2c7b9fdd | ||
|
|
324f0ed7cc | ||
|
|
a3b7a0525a | ||
|
|
e046b09c8a | ||
|
|
e44cd671b2 | ||
|
|
4a829cb51b | ||
|
|
31e6ec3c5b | ||
|
|
1647d0090e | ||
|
|
39ed016a1e | ||
|
|
d88ce7181c | ||
|
|
53684af0e3 | ||
|
|
59688d7cd5 | ||
|
|
963e14c685 | ||
|
|
4b934c784d | ||
|
|
f959f58e15 | ||
|
|
df97fd33cf | ||
|
|
7f5f7017db | ||
|
|
3d477d229d | ||
|
|
966fa1be89 | ||
|
|
2010dda6fa | ||
|
|
cadcc69bc0 | ||
|
|
3fa54559e5 | ||
|
|
f87a43487e | ||
|
|
3b8842e618 | ||
|
|
c4c713ca6e | ||
|
|
9b918bba2f | ||
|
|
52f6bfc2df | ||
|
|
b49c1ab4b3 | ||
|
|
d518868691 | ||
|
|
f9f7e29295 | ||
|
|
a462966cf6 | ||
|
|
6b52f90ad6 | ||
|
|
8a0c06ba9e | ||
|
|
714fd8c915 | ||
|
|
addd26ae55 | ||
|
|
606761b3c7 | ||
|
|
eb1156b099 | ||
|
|
6a81cc9d8c | ||
|
|
4c18a04439 | ||
|
|
d65d7fd4d0 | ||
|
|
57f2d9e31c | ||
|
|
6e40cb12ab | ||
|
|
9de1657aab | ||
|
|
7c0ac467bb | ||
|
|
76cb1c64ce | ||
|
|
ddff67acb3 | ||
|
|
1aebd55f26 | ||
|
|
0eaa0ecd86 | ||
|
|
9fdc6e0e82 | ||
|
|
47d3a1213c | ||
|
|
f5f5423cc9 | ||
|
|
c256bfc02e | ||
|
|
7a3932efc0 | ||
|
|
532cb35aa8 | ||
|
|
eba8bd4faa | ||
|
|
0d40dd42b1 | ||
|
|
a92f25b48e | ||
|
|
0707695146 | ||
|
|
70d099bbd6 | ||
|
|
aac6398517 | ||
|
|
1fdad90b4d | ||
|
|
f877980da9 | ||
|
|
7dfd61f462 | ||
|
|
02114ea073 | ||
|
|
1b8840d333 | ||
|
|
8796c3b27e | ||
|
|
077f58b228 | ||
|
|
ccb1daf30a | ||
|
|
2b282ead09 | ||
|
|
5ea76789c6 | ||
|
|
47a7f83a0b | ||
|
|
a5155f55e7 | ||
|
|
730e760666 | ||
|
|
5c2fba0398 | ||
|
|
201ae0dac2 | ||
|
|
4942578de0 | ||
|
|
faab77cc79 | ||
|
|
077a45cc93 | ||
|
|
7526f50706 | ||
|
|
7e1bc59f80 | ||
|
|
05951a23a5 | ||
|
|
757ff8a7d4 | ||
|
|
9440956575 | ||
|
|
a54b401ee2 | ||
|
|
0a4a76c4c4 | ||
|
|
c50875ccf6 | ||
|
|
aa47937d4f | ||
|
|
1ca3df877b | ||
|
|
38b3290516 | ||
|
|
576eeb70dd | ||
|
|
416240e80e | ||
|
|
0befbfd766 | ||
|
|
5ddab17089 | ||
|
|
da86ebca31 | ||
|
|
039ac7c7c4 | ||
|
|
0e68c9a720 | ||
|
|
a83a6c38e4 | ||
|
|
2deb356356 | ||
|
|
19c2d24c29 | ||
|
|
5028f17f64 | ||
|
|
309a1cb337 | ||
|
|
2179db3692 | ||
|
|
7e5d8f37fd | ||
|
|
cc153888d5 | ||
|
|
5cc0abbb02 | ||
|
|
12b55bb8cc | ||
|
|
95f29f457a | ||
|
|
8aeaaf06ee | ||
|
|
70b933f405 | ||
|
|
12419b75f2 | ||
|
|
a99513a43e | ||
|
|
76f5e3659e | ||
|
|
8175a53826 | ||
|
|
93634dd822 | ||
|
|
6c1d481135 | ||
|
|
037a0be3ae | ||
|
|
e0fede621e | ||
|
|
32058ed268 | ||
|
|
b80692f602 | ||
|
|
9dc9477e71 | ||
|
|
eca63ab32a | ||
|
|
579d2477f5 | ||
|
|
d646d06e91 | ||
|
|
fdcd847f4a | ||
|
|
870a33247b | ||
|
|
aa140f0fa7 | ||
|
|
7c01a9c7b9 | ||
|
|
3f83b22de2 | ||
|
|
55baf93635 | ||
|
|
099e35103f | ||
|
|
409de18a65 | ||
|
|
b5dd5adb08 | ||
|
|
1c06a8b1ef | ||
|
|
d2c39d2e45 | ||
|
|
8403eab2c4 | ||
|
|
17a03ebc11 | ||
|
|
1842f30359 | ||
|
|
157a85451a | ||
|
|
b893356c49 | ||
|
|
c21b4e4d56 | ||
|
|
54729f9ef3 | ||
|
|
6724de54e6 | ||
|
|
ef975307c4 | ||
|
|
5ff63b66a0 | ||
|
|
3cf068670c | ||
|
|
875295320e | ||
|
|
bae38a9fdf | ||
|
|
4b53c6b975 | ||
|
|
9129a414e3 | ||
|
|
28b5262976 | ||
|
|
fa20b0aff2 | ||
|
|
a4ec017d1c | ||
|
|
8c54675750 | ||
|
|
0636b3247d | ||
|
|
8e2711681c | ||
|
|
99634638ba | ||
|
|
ce2b4fe634 | ||
|
|
cbfecea112 | ||
|
|
53adc9d84a | ||
|
|
18ebd9ac21 | ||
|
|
3bbbe640c0 | ||
|
|
922213f3d9 | ||
|
|
334bfd50ff | ||
|
|
facda17945 | ||
|
|
12ff119841 | ||
|
|
e3ab10196e | ||
|
|
1ec6782f32 | ||
|
|
bf0adc1fdf | ||
|
|
27659c85e6 | ||
|
|
70c329e235 | ||
|
|
95af3c63ac | ||
|
|
0fef5022f0 | ||
|
|
4ec70d7851 | ||
|
|
6cd19894e9 | ||
|
|
0e11d81adb | ||
|
|
f120877756 | ||
|
|
4d696d69a0 | ||
|
|
3fda77227e | ||
|
|
44490664f5 | ||
|
|
6daa152c0c | ||
|
|
b3d4ce291e | ||
|
|
9d4f163e88 | ||
|
|
21f058a9f7 | ||
|
|
386da502ba | ||
|
|
d5ebe542f9 | ||
|
|
27bc2e012d | ||
|
|
ef7f3230ec | ||
|
|
4dbc77dbbb | ||
|
|
697da99f8f | ||
|
|
5ca275876b | ||
|
|
5dc1a583b4 | ||
|
|
595edc8d46 | ||
|
|
c1db282fcd | ||
|
|
286ef6aa87 | ||
|
|
361c220846 | ||
|
|
3c01dcb01f | ||
|
|
2b1e645eb4 | ||
|
|
293a5066d2 | ||
|
|
48cd1262fe | ||
|
|
8f2561e289 | ||
|
|
f1d8272693 | ||
|
|
1ba48f8627 | ||
|
|
ec77338e0c | ||
|
|
20e6989c73 | ||
|
|
704357467a | ||
|
|
f70656cdd7 | ||
|
|
e6127a278e | ||
|
|
7688db2628 | ||
|
|
145a9fb891 | ||
|
|
2b38d46fb5 | ||
|
|
6c2937affc | ||
|
|
96de888ac7 | ||
|
|
0dce32405f | ||
|
|
c1637e97b2 | ||
|
|
930678cc0e | ||
|
|
53a30a2d42 | ||
|
|
62c755c1d5 | ||
|
|
6d2b596bb4 | ||
|
|
e70406d5a1 | ||
|
|
00b7205a3c | ||
|
|
de0beeff9f | ||
|
|
819e4eb540 | ||
|
|
ccf2521a6d | ||
|
|
6ecd7ddddf | ||
|
|
a82cc3bcbf | ||
|
|
00fdf5ce0a | ||
|
|
bb6655e696 | ||
|
|
bb0a181056 | ||
|
|
e7bd8ed619 | ||
|
|
70080b112a | ||
|
|
681bc2ef07 | ||
|
|
02055ba4eb | ||
|
|
8fd083a1ea | ||
|
|
addfea6614 | ||
|
|
ec3ed0e4ae | ||
|
|
f492aa7ac9 | ||
|
|
99c9cbf470 | ||
|
|
d22ab320e2 | ||
|
|
0121e09907 | ||
|
|
7d17fb5c6c | ||
|
|
a4837da899 | ||
|
|
3a75c8a752 | ||
|
|
6c8327e39f | ||
|
|
14167ed778 | ||
|
|
237706e39f | ||
|
|
c48e404e63 | ||
|
|
ccacfc1047 | ||
|
|
ccf07c4b21 | ||
|
|
6f7c9b7d0f |
@@ -0,0 +1 @@
|
||||
ChangeLog linguist-language=Markdown
|
||||
@@ -1,49 +0,0 @@
|
||||
_We will be very grateful, if your problem was described as completely as possible,
|
||||
enclosing excerpts from logs (if possible within DEBUG mode, if no errors evident
|
||||
within INFO mode), and configuration in particular of effected relevant settings
|
||||
(e.g., with ` fail2ban-client -d | grep 'affected-jail-name' ` for a particular
|
||||
jail troubleshooting).
|
||||
Thank you in advance for the details, because such issues like "It does not work"
|
||||
alone could not help to resolve anything!
|
||||
Thanks! (remove this paragraph and other comments upon reading)_
|
||||
|
||||
### Environment:
|
||||
|
||||
_Fill out and check (`[x]`) the boxes which apply. If your Fail2Ban version is outdated,
|
||||
and you can't verify that the issue persists in the recent release, better seek support
|
||||
from the distribution you obtained Fail2Ban from_
|
||||
|
||||
- Fail2Ban version (including any possible distribution suffixes):
|
||||
- OS, including release name/version:
|
||||
- [ ] Fail2Ban installed via OS/distribution mechanisms
|
||||
- [ ] You have not applied any additional foreign patches to the codebase
|
||||
- [ ] Some customizations were done to the configuration (provide details below is so)
|
||||
|
||||
### The issue:
|
||||
|
||||
_Summary here_
|
||||
|
||||
#### Steps to reproduce
|
||||
|
||||
#### Expected behavior
|
||||
|
||||
#### Observed behavior
|
||||
|
||||
#### Any additional information
|
||||
|
||||
### Configuration, dump and another helpful excerpts
|
||||
|
||||
#### Any customizations done to /etc/fail2ban/ configuration
|
||||
```
|
||||
```
|
||||
|
||||
#### Relevant parts of /var/log/fail2ban.log file:
|
||||
_preferably obtained while running fail2ban with `loglevel = 4`_
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
#### Relevant lines from monitored log files in question:
|
||||
|
||||
```
|
||||
```
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a bug within the fail2ban engines (not filters or jails)
|
||||
title: '[BR]: '
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
- Before reporting, please make sure to search the open and closed issues for any reports in the past.
|
||||
- Use this issue template to report a bug in the fail2ban engine (not in a filter or jail).
|
||||
- If you want to request a feature or a new filter, please use "Feature request" or "Filter request" instead.
|
||||
- If you have rather some question, please open or join to some discussion.
|
||||
|
||||
We will be very grateful, if your problem was described as completely as possible,
|
||||
enclosing excerpts from logs (if possible within DEBUG mode, if no errors evident
|
||||
within INFO mode), and configuration in particular of effected relevant settings
|
||||
(e.g., with ` fail2ban-client -d | grep 'affected-jail-name' ` for a particular
|
||||
jail troubleshooting).
|
||||
Thank you in advance for the details, because such issues like "It does not work"
|
||||
alone could not help to resolve anything!
|
||||
Thanks!
|
||||
(you can remove this paragraph and other comments upon reading)
|
||||
-->
|
||||
|
||||
### Environment:
|
||||
|
||||
<!--
|
||||
Fill out and check (`[x]`) the boxes which apply. If your Fail2Ban version is outdated,
|
||||
and you can't verify that the issue persists in the recent release, better seek support
|
||||
from the distribution you obtained Fail2Ban from
|
||||
-->
|
||||
|
||||
- Fail2Ban version <!-- including any possible distribution suffixes --> :
|
||||
- OS, including release name/version :
|
||||
- [ ] Fail2Ban installed via OS/distribution mechanisms
|
||||
- [ ] You have not applied any additional foreign patches to the codebase
|
||||
- [ ] Some customizations were done to the configuration (provide details below is so)
|
||||
|
||||
### The issue:
|
||||
|
||||
<!-- summary here -->
|
||||
|
||||
#### Steps to reproduce
|
||||
|
||||
#### Expected behavior
|
||||
|
||||
#### Observed behavior
|
||||
|
||||
#### Any additional information
|
||||
|
||||
|
||||
### Configuration, dump and another helpful excerpts
|
||||
|
||||
#### Any customizations done to /etc/fail2ban/ configuration
|
||||
<!-- put your configuration excerpts between next 2 lines -->
|
||||
```
|
||||
```
|
||||
|
||||
#### Relevant parts of /var/log/fail2ban.log file:
|
||||
<!-- preferably obtained while running fail2ban with `loglevel = 4` -->
|
||||
<!-- put your log excerpt between next 2 lines -->
|
||||
```
|
||||
```
|
||||
|
||||
#### Relevant lines from monitored log files:
|
||||
<!-- put your log excerpt between next 2 lines -->
|
||||
```
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea or an enhancement for this project
|
||||
title: '[RFE]: '
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
- Before requesting, please make sure to search the open and closed issues for any requests in the past.
|
||||
- Use this issue template to request a feature in the fail2ban engine (not a new filter or jail).
|
||||
- If you want to request a new filter or failregex, please use "Filter request" instead.
|
||||
- If you have rather some question, please open or join to some discussion.
|
||||
-->
|
||||
|
||||
#### Feature request type
|
||||
<!--
|
||||
Please provide a summary description of the feature request.
|
||||
-->
|
||||
|
||||
#### Description
|
||||
<!--
|
||||
Please describe the feature in more detail.
|
||||
-->
|
||||
|
||||
#### Considered alternatives
|
||||
<!--
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
-->
|
||||
|
||||
#### Any additional information
|
||||
<!--
|
||||
Add any other context or screenshots about the feature request here.
|
||||
-->
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: Filter request
|
||||
about: Request a new jail or filter to be supported or existing filter extended with new failregex
|
||||
title: '[FR]: '
|
||||
labels: filter-request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
- Before requesting, please make sure to search the open and closed issues for any requests in the past.
|
||||
- Sometimes failregex have been already requested before but are not implemented yet due to various reasons.
|
||||
- If there are no hits for your concerns, please proceed otherwise add a comment to the related issue (also if it is closed).
|
||||
- If you want to request a new feature, please use "Feature request" instead.
|
||||
- If you have rather some question, please open or join to some discussion.
|
||||
-->
|
||||
|
||||
### Environment:
|
||||
|
||||
<!--
|
||||
Fill out and check (`[x]`) the boxes which apply.
|
||||
-->
|
||||
|
||||
- Fail2Ban version <!-- including any possible distribution suffixes --> :
|
||||
- OS, including release name/version :
|
||||
|
||||
#### Service, project or product which log or journal should be monitored
|
||||
|
||||
- Name of filter or jail in Fail2Ban (if already exists) :
|
||||
- Service, project or product name, including release name/version :
|
||||
- Repository or URL (if known) :
|
||||
- Service type :
|
||||
- Ports and protocols the service is listening :
|
||||
|
||||
#### Log or journal information
|
||||
<!-- Delete unrelated group -->
|
||||
|
||||
<!-- Log file -->
|
||||
|
||||
- Log file name(s) :
|
||||
|
||||
<!-- Systemd journal -->
|
||||
|
||||
- Journal identifier or unit name :
|
||||
|
||||
#### Any additional information
|
||||
|
||||
|
||||
### Relevant lines from monitored log files:
|
||||
|
||||
#### failures in sense of fail2ban filter (fail2ban must match):
|
||||
<!-- put your log excerpt between next 2 lines -->
|
||||
```
|
||||
```
|
||||
|
||||
#### legitimate messages (fail2ban should not consider as failures):
|
||||
<!-- put your log excerpt between next 2 lines -->
|
||||
```
|
||||
```
|
||||
@@ -1,7 +1,8 @@
|
||||
Before submitting your PR, please review the following checklist:
|
||||
|
||||
- [ ] **CHOOSE CORRECT BRANCH**: if filing a bugfix/enhancement
|
||||
against 0.9.x series, choose `master` branch
|
||||
against certain release version, choose `0.9`, `0.10` or `0.11` branch,
|
||||
for dev-edition use `master` branch
|
||||
- [ ] **CONSIDER adding a unit test** if your PR resolves an issue
|
||||
- [ ] **LIST ISSUES** this PR resolves
|
||||
- [ ] **MAKE SURE** this PR doesn't break existing tests
|
||||
|
||||
+44
-12
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, pypy2, pypy3]
|
||||
python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, '3.10', '3.11.0-alpha.1', pypy2, pypy3]
|
||||
fail-fast: false
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
@@ -33,34 +33,66 @@ jobs:
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
|
||||
- name: Grant systemd-journal access
|
||||
run: sudo usermod -a -G systemd-journal "$USER" || echo 'no systemd-journal access'
|
||||
|
||||
- name: Python version
|
||||
run: |
|
||||
F2B_PY=$(python -c "import sys; print(sys.version)")
|
||||
echo "Python: ${{ matrix.python-version }} -- $F2B_PY"
|
||||
echo "Python: ${{ matrix.python-version }} -- ${F2B_PY/$'\n'/ }"
|
||||
F2B_PYV=$(echo "${F2B_PY}" | grep -oP '^\d+(?:\.\d+)')
|
||||
F2B_PY=${F2B_PY:0:1}
|
||||
echo "Set F2B_PY=$F2B_PY"
|
||||
echo "Set F2B_PY=$F2B_PY, F2B_PYV=$F2B_PYV"
|
||||
echo "F2B_PY=$F2B_PY" >> $GITHUB_ENV
|
||||
echo "F2B_PYV=$F2B_PYV" >> $GITHUB_ENV
|
||||
# for GHA we need to monitor all journals, since it cannot be found using SYSTEM_ONLY(4):
|
||||
echo "F2B_SYSTEMD_DEFAULT_FLAGS=0" >> $GITHUB_ENV
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
if [[ "$F2B_PY" = 3 ]]; then python -m pip install --upgrade pip || echo "can't upgrade pip"; fi
|
||||
if [[ "$F2B_PY" = 3 ]] && ! command -v 2to3x -v 2to3 > /dev/null; then
|
||||
pip install 2to3
|
||||
#pip install 2to3
|
||||
sudo apt-get -y install 2to3
|
||||
fi
|
||||
pip install systemd-python || echo 'systemd not available'
|
||||
pip install pyinotify || echo 'inotify not available'
|
||||
#sudo apt-get -y install python${F2B_PY/2/}-pyinotify || echo 'inotify not available'
|
||||
python -m pip install pyinotify || echo 'inotify not available'
|
||||
#sudo apt-get -y install python${F2B_PY/2/}-systemd || echo 'systemd not available'
|
||||
sudo apt-get -y install libsystemd-dev || echo 'systemd dependencies seems to be unavailable'
|
||||
python -m pip install systemd-python || echo 'systemd not available'
|
||||
|
||||
- name: Before scripts
|
||||
run: |
|
||||
cd "$GITHUB_WORKSPACE"
|
||||
# Manually execute 2to3 for now
|
||||
if [[ "$F2B_PY" = 3 ]]; then echo "2to3 ..." && ./fail2ban-2to3; fi
|
||||
_debug() { echo -n "$1 "; err=$("${@:2}" 2>&1) && echo 'OK' || echo -e "FAIL\n$err"; }
|
||||
# (debug) output current preferred encoding:
|
||||
python -c 'import locale, sys; from fail2ban.helpers import PREFER_ENC; print(PREFER_ENC, locale.getpreferredencoding(), (sys.stdout and sys.stdout.encoding))'
|
||||
|
||||
_debug 'Encodings:' python -c 'import locale, sys; from fail2ban.helpers import PREFER_ENC; print(PREFER_ENC, locale.getpreferredencoding(), (sys.stdout and sys.stdout.encoding))'
|
||||
# (debug) backend availabilities:
|
||||
echo 'Backends:'
|
||||
_debug '- systemd:' python -c 'from fail2ban.server.filtersystemd import FilterSystemd'
|
||||
#_debug '- systemd (root): ' sudo python -c 'from fail2ban.server.filtersystemd import FilterSystemd'
|
||||
_debug '- pyinotify:' python -c 'from fail2ban.server.filterpyinotify import FilterPyinotify'
|
||||
|
||||
- name: Test suite
|
||||
run: if [[ "$F2B_PY" = 2 ]]; then python setup.py test; else python bin/fail2ban-testcases --verbosity=2; fi
|
||||
|
||||
run: |
|
||||
if [[ "$F2B_PY" = 2 ]]; then
|
||||
python setup.py test
|
||||
elif dpkg --compare-versions "$F2B_PYV" lt 3.10; then
|
||||
python bin/fail2ban-testcases --verbosity=2
|
||||
else
|
||||
echo "Skip systemd backend since systemd-python module must be fixed for python >= v.3.10 in GHA ..."
|
||||
python bin/fail2ban-testcases --verbosity=2 -i "[sS]ystemd|[jJ]ournal"
|
||||
fi
|
||||
|
||||
#- name: Test suite (debug some systemd tests only)
|
||||
#run: python bin/fail2ban-testcases --verbosity=2 "[sS]ystemd|[jJ]ournal"
|
||||
#run: python bin/fail2ban-testcases --verbosity=2 -l 5 "test_WrongChar"
|
||||
|
||||
- name: Build
|
||||
run: python setup.py build
|
||||
|
||||
#- name: Test initd scripts
|
||||
# run: shellcheck -s bash -e SC1090,SC1091 files/debian-initd
|
||||
|
||||
+1
-9
@@ -10,16 +10,8 @@ dist: xenial
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- python: 2.6
|
||||
dist: trusty # required for Python 2.6
|
||||
- python: 2.7
|
||||
dist: trusty # required for packages like gamin
|
||||
name: 2.7 (trusty)
|
||||
- python: 2.7
|
||||
name: 2.7 (xenial)
|
||||
- python: pypy
|
||||
- python: 3.3
|
||||
dist: trusty
|
||||
#- python: pypy
|
||||
- python: 3.4
|
||||
- python: 3.5
|
||||
- python: 3.6
|
||||
|
||||
@@ -6,30 +6,69 @@
|
||||
Fail2Ban: Changelog
|
||||
===================
|
||||
|
||||
ver. 0.10.6 (2020/11/23) - just-what-the-doctor-ordered
|
||||
ver. 1.0.1-dev-1 (20??/??/??) - development nightly edition
|
||||
-----------
|
||||
|
||||
### Incompatibility list (v.0.10 compared to v.0.9):
|
||||
* Filter (or `failregex`) internal capture-groups:
|
||||
### Compatibility:
|
||||
* potential incompatibility by parsing of options of `backend`, `filter` and `action` parameters (if they
|
||||
are partially incorrect), because fail2ban could throw an error now (doesn't silently bypass it anymore).
|
||||
* to v.0.11:
|
||||
- due to change of `actioncheck` behavior (gh-488), some actions can be incompatible as regards
|
||||
the invariant check, if `actionban` or `actionunban` would not throw an error (exit code
|
||||
different from 0) in case of unsane environment.
|
||||
|
||||
- If you've your own `failregex` or custom filters using conditional match `(?P=host)`, you should
|
||||
rewrite the regex like in example below resp. using `(?:(?P=ip4)|(?P=ip6)` instead of `(?P=host)`
|
||||
(or `(?:(?P=ip4)|(?P=ip6)|(?P=dns))` corresponding your `usedns` and `raw` settings).
|
||||
### Fixes
|
||||
* readline fixed to consider interim new-line character as part of code point in multi-byte logs
|
||||
(e. g. unicode encoding like utf-16be, utf-16le);
|
||||
* `action.d/ufw.conf`:
|
||||
- fixed handling on IPv6 (using prepend, gh-2331, gh-3018)
|
||||
- application names containing spaces can be used now (gh-656, gh-1532, gh-3018)
|
||||
* `filter.d/drupal-auth.conf` more strict regex, extended to match "Login attempt failed from" (gh-2742)
|
||||
|
||||
Of course you can always define your own capture-group (like below `_cond_ip_`) to do this.
|
||||
```
|
||||
testln="1500000000 failure from 192.0.2.1: bad host 192.0.2.1"
|
||||
fail2ban-regex "$testln" "^\s*failure from (?P<_cond_ip_><HOST>): bad host (?P=_cond_ip_)$"
|
||||
```
|
||||
- New internal groups (currently reserved for internal usage):
|
||||
`ip4`, `ip6`, `dns`, `fid`, `fport`, additionally `user` and another captures in lower case if
|
||||
mapping from tag `<F-*>` used in failregex (e. g. `user` by `<F-USER>`).
|
||||
### New Features and Enhancements
|
||||
* `actioncheck` behavior is changed now (gh-488), so invariant check as well as restore or repair
|
||||
of sane environment (in case of recognized unsane state) would only occur on action errors (e. g.
|
||||
if ban or unban operations are exiting with other code as 0)
|
||||
* better recognition of log rotation, better performance by reopen: avoid unnecessary seek to begin of file
|
||||
(and hash calculation)
|
||||
* file filter reads only complete lines (ended with new-line) now, so waits for end of line (for its completion)
|
||||
* `action.d/ufw.conf` (gh-3018):
|
||||
- new option `add` (default `prepend`), can be supplied as `insert 1` for ufw versions before v.0.36 (gh-2331, gh-3018)
|
||||
- new options `kill-mode` and `kill` to drop established connections of intruder (see action for details, gh-3018)
|
||||
* `filter.d/nginx-http-auth.conf` - extended with parameter mode, so additionally to `auth` (or `normal`)
|
||||
mode `fallback` (or combined as `aggressive`) can find SSL errors while SSL handshaking, gh-2881
|
||||
|
||||
* v.0.10 uses more precise date template handling, that can be theoretically incompatible to some
|
||||
user configurations resp. `datepattern`.
|
||||
|
||||
* Since v0.10 fail2ban supports the matching of IPv6 addresses, but not all ban actions are
|
||||
IPv6-capable now.
|
||||
ver. 0.11.2 (2020/11/23) - heal-the-world-with-security-tools
|
||||
-----------
|
||||
|
||||
### Compatibility:
|
||||
* to v.0.10:
|
||||
- 0.11 is totally compatible to 0.10 (configuration- and API-related stuff), but the database
|
||||
got some new tables and fields (auto-converted during the first start), so once updated to 0.11, you
|
||||
have to remove the database /var/lib/fail2ban/fail2ban.sqlite3 (or its different to 0.10 schema)
|
||||
if you would need to downgrade to 0.10 for some reason.
|
||||
* to v.0.9:
|
||||
- Filter (or `failregex`) internal capture-groups:
|
||||
|
||||
* If you've your own `failregex` or custom filters using conditional match `(?P=host)`, you should
|
||||
rewrite the regex like in example below resp. using `(?:(?P=ip4)|(?P=ip6)` instead of `(?P=host)`
|
||||
(or `(?:(?P=ip4)|(?P=ip6)|(?P=dns))` corresponding your `usedns` and `raw` settings).
|
||||
|
||||
Of course you can always define your own capture-group (like below `_cond_ip_`) to do this.
|
||||
```
|
||||
testln="1500000000 failure from 192.0.2.1: bad host 192.0.2.1"
|
||||
fail2ban-regex "$testln" "^\s*failure from (?P<_cond_ip_><HOST>): bad host (?P=_cond_ip_)$"
|
||||
```
|
||||
* New internal groups (currently reserved for internal usage):
|
||||
`ip4`, `ip6`, `dns`, `fid`, `fport`, additionally `user` and another captures in lower case if
|
||||
mapping from tag `<F-*>` used in failregex (e. g. `user` by `<F-USER>`).
|
||||
|
||||
- v.0.10 and 0.11 use more precise date template handling, that can be theoretically incompatible to some
|
||||
user configurations resp. `datepattern`.
|
||||
|
||||
- Since v0.10 fail2ban supports the matching of IPv6 addresses, but not all ban actions are
|
||||
IPv6-capable now.
|
||||
|
||||
### Fixes
|
||||
* [stability] prevent race condition - no ban if filter (backend) is continuously busy if
|
||||
@@ -91,6 +130,36 @@ ver. 0.10.6 (2020/11/23) - just-what-the-doctor-ordered
|
||||
whereas filter will use now as timestamp (gh-2802)
|
||||
* performance optimization of `datepattern` (better search algorithm in datedetector, especially for single template);
|
||||
* fail2ban-client: extended to unban IP range(s) by subnet (CIDR/mask) or hostname (DNS), gh-2791;
|
||||
* extended capturing of alternate tags in filter, allowing combine of multiple groups to single tuple token with new tag
|
||||
prefix `<F-TUPLE_`, that would combine value of `<F-V>` with all value of `<F-TUPLE_V?_n?>` tags (gh-2755)
|
||||
|
||||
|
||||
ver. 0.11.1 (2020/01/11) - this-is-the-way
|
||||
-----------
|
||||
|
||||
### Fixes
|
||||
* purge database will be executed now (within observer).
|
||||
* restoring currently banned ip after service restart fixed
|
||||
(now < timeofban + bantime), ignore old log failures (already banned)
|
||||
* upgrade database: update new created table `bips` with entries from table `bans` (allows restore
|
||||
current bans after upgrade from version <= 0.10)
|
||||
|
||||
### New Features
|
||||
* Increment ban time (+ observer) functionality introduced.
|
||||
* Database functionality extended with bad ips.
|
||||
* New tags (usable in actions):
|
||||
- `<bancount>` - ban count of this offender if known as bad (started by 1 for unknown)
|
||||
- `<bantime>` - current ban-time of the ticket (prolongation can be retarded up to 10 sec.)
|
||||
* Introduced new action command `actionprolong` to prolong ban-time (e. g. set new timeout if expected);
|
||||
Several actions (like ipset, etc.) rewritten using net logic with `actionprolong`.
|
||||
Note: because ban-time is dynamic, it was removed from jail.conf as timeout argument (check jail.local).
|
||||
|
||||
### Enhancements
|
||||
* algorithm of restore current bans after restart changed: update the restored ban-time (and therefore
|
||||
end of ban) of the ticket with ban-time of jail (as maximum), for all tickets with ban-time greater
|
||||
(or persistent); not affected if ban-time of the jail is unchanged between stop/start.
|
||||
* added new setup-option `--without-tests` to skip building and installing of tests files (gh-2287).
|
||||
* added new command `fail2ban-client get <JAIL> banip ?sep-char|--with-time?` to get the banned ip addresses (gh-1916).
|
||||
|
||||
|
||||
ver. 0.10.5 (2020/01/10) - deserve-more-respect-a-jedis-weapon-must
|
||||
@@ -153,7 +222,7 @@ Yes, Hrrrm...
|
||||
### New Features
|
||||
* new replacement tags for failregex to match subnets in form of IP-addresses with CIDR mask (gh-2559):
|
||||
- `<CIDR>` - helper regex to match CIDR (simple integer form of net-mask);
|
||||
- `<SUBNET>` - regex to match sub-net adresses (in form of IP/CIDR, also single IP is matched, so part /CIDR is optional);
|
||||
- `<SUBNET>` - regex to match sub-net addresses (in form of IP/CIDR, also single IP is matched, so part /CIDR is optional);
|
||||
* grouped tags (`<ADDR>`, `<HOST>`, `<SUBNET>`) recognize IP addresses enclosed in square brackets
|
||||
* new failregex-flag tag `<F-MLFGAINED>` for failregex, signaled that the access to service was gained
|
||||
(ATM used similar to tag `<F-NOFAIL>`, but it does not add the log-line to matches, gh-2279)
|
||||
@@ -471,9 +540,14 @@ TODO: implementing of options resp. other tasks from PR #1346
|
||||
- `<fid>` - failure identifier (if raw resp. failures without IP address)
|
||||
- `<ip-rev>` - PTR reversed representation of IP address
|
||||
- `<ip-host>` - host name of the IP address
|
||||
- `<bancount>` - ban count of this offender if known as bad (started by 1 for unknown)
|
||||
- `<bantime>` - current ban-time of the ticket (prolongation can be retarded up to 10 sec.)
|
||||
- `<F-...>` - interpolates to the corresponding filter group capture `...`
|
||||
- `<fq-hostname>` - fully-qualified name of host (the same as `$(hostname -f)`)
|
||||
- `<sh-hostname>` - short hostname (the same as `$(uname -n)`)
|
||||
* Introduced new action command `actionprolong` to prolong ban-time (e. g. set new timeout if expected);
|
||||
Several actions (like ipset, etc.) rewritten using net logic with `actionprolong`.
|
||||
Note: because ban-time is dynamic, it was removed from jail.conf as timeout argument (check jail.local).
|
||||
* Allow to use filter options by `fail2ban-regex`, example:
|
||||
fail2ban-regex text.log "sshd[mode=aggressive]"
|
||||
* Samples test case factory extended with filter options - dict in JSON to control
|
||||
@@ -547,6 +621,9 @@ ver. 0.10.0-alpha-1 (2016/07/14) - ipv6-support-etc
|
||||
* testSocket: sporadical bug repaired - wait for server thread starts a socket (listener)
|
||||
* testExecuteTimeoutWithNastyChildren: sporadical bug repaired - wait for pid file inside bash,
|
||||
kill tree in any case (gh-1155)
|
||||
* purge database will be executed now (within observer).
|
||||
* restoring currently banned ip after service restart fixed
|
||||
(now < timeofban + bantime), ignore old log failures (already banned)
|
||||
* Fixed high-load of pyinotify-backend,
|
||||
see https://github.com/fail2ban/fail2ban/issues/885#issuecomment-248964591
|
||||
* Database: stability fix - repack cursor iterator as long as locked
|
||||
@@ -584,6 +661,9 @@ ver. 0.10.0-alpha-1 (2016/07/14) - ipv6-support-etc
|
||||
- new conditional section functionality used in config resp. includes:
|
||||
- [Init?family=inet4] - IPv4 qualified hosts only
|
||||
- [Init?family=inet6] - IPv6 qualified hosts only
|
||||
* Increment ban time (+ observer) functionality introduced.
|
||||
Thanks Serg G. Brester (sebres)
|
||||
* Database functionality extended with bad ips.
|
||||
* New reload functionality (now totally without restart, unbanning/rebanning, etc.),
|
||||
see gh-1557
|
||||
* Several commands extended and new commands introduced:
|
||||
|
||||
@@ -278,6 +278,7 @@ to tune it. fail2ban-regex -D ... will present Debuggex URLs for the regexs
|
||||
and sample log files that you pass into it.
|
||||
|
||||
In general use when using regex debuggers for generating fail2ban filters:
|
||||
|
||||
* use regex from the ./fail2ban-regex output (to ensure all substitutions are
|
||||
done)
|
||||
* replace <HOST> with (?&.ipv4)
|
||||
|
||||
@@ -3,10 +3,9 @@ bin/fail2ban-regex
|
||||
bin/fail2ban-server
|
||||
bin/fail2ban-testcases
|
||||
ChangeLog
|
||||
config/action.d/apprise.conf
|
||||
config/action.d/abuseipdb.conf
|
||||
config/action.d/apf.conf
|
||||
config/action.d/badips.conf
|
||||
config/action.d/badips.py
|
||||
config/action.d/blocklist_de.conf
|
||||
config/action.d/bsd-ipfw.conf
|
||||
config/action.d/cloudflare.conf
|
||||
@@ -210,6 +209,7 @@ fail2ban/server/jail.py
|
||||
fail2ban/server/jails.py
|
||||
fail2ban/server/jailthread.py
|
||||
fail2ban/server/mytime.py
|
||||
fail2ban/server/observer.py
|
||||
fail2ban/server/server.py
|
||||
fail2ban/server/strptime.py
|
||||
fail2ban/server/ticket.py
|
||||
@@ -219,7 +219,6 @@ fail2ban/setup.py
|
||||
fail2ban-testcases-all
|
||||
fail2ban-testcases-all-python3
|
||||
fail2ban/tests/action_d/__init__.py
|
||||
fail2ban/tests/action_d/test_badips.py
|
||||
fail2ban/tests/action_d/test_smtp.py
|
||||
fail2ban/tests/actionstestcase.py
|
||||
fail2ban/tests/actiontestcase.py
|
||||
@@ -266,6 +265,7 @@ fail2ban/tests/files/config/apache-auth/digest_wrongrelm/.htpasswd
|
||||
fail2ban/tests/files/config/apache-auth/noentry/.htaccess
|
||||
fail2ban/tests/files/config/apache-auth/README
|
||||
fail2ban/tests/files/database_v1.db
|
||||
fail2ban/tests/files/database_v2.db
|
||||
fail2ban/tests/files/filter.d/substition.conf
|
||||
fail2ban/tests/files/filter.d/testcase01.conf
|
||||
fail2ban/tests/files/filter.d/testcase02.conf
|
||||
@@ -376,6 +376,7 @@ fail2ban/tests/files/zzz-sshd-obsolete-multiline.log
|
||||
fail2ban/tests/filtertestcase.py
|
||||
fail2ban/tests/__init__.py
|
||||
fail2ban/tests/misctestcase.py
|
||||
fail2ban/tests/observertestcase.py
|
||||
fail2ban/tests/samplestestcase.py
|
||||
fail2ban/tests/servertestcase.py
|
||||
fail2ban/tests/sockettestcase.py
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/ _|__ _(_) |_ ) |__ __ _ _ _
|
||||
| _/ _` | | |/ /| '_ \/ _` | ' \
|
||||
|_| \__,_|_|_/___|_.__/\__,_|_||_|
|
||||
v0.10.3.dev1 20??/??/??
|
||||
v1.0.1.dev1 20??/??/??
|
||||
|
||||
## Fail2Ban: ban hosts that cause multiple authentication errors
|
||||
|
||||
@@ -18,11 +18,12 @@ attempts, it cannot eliminate the risk presented by weak authentication.
|
||||
Set up services to use only two factor, or public/private authentication
|
||||
mechanisms if you really want to protect services.
|
||||
|
||||
<img src="http://www.worldipv6launch.org/wp-content/themes/ipv6/downloads/World_IPv6_launch_logo.svg" height="52pt"/> | Since v0.10 fail2ban supports the matching of the IPv6 addresses.
|
||||
<img src="http://www.worldipv6launch.org/wp-content/themes/ipv6/downloads/World_IPv6_launch_logo.svg" height="52pt"/> | Since v0.10 fail2ban supports the matching of IPv6 addresses.
|
||||
------|------
|
||||
|
||||
This README is a quick introduction to Fail2Ban. More documentation, FAQ, and HOWTOs
|
||||
to be found on fail2ban(1) manpage, [Wiki](https://github.com/fail2ban/fail2ban/wiki)
|
||||
to be found on fail2ban(1) manpage, [Wiki](https://github.com/fail2ban/fail2ban/wiki),
|
||||
[Developers documentation](https://fail2ban.readthedocs.io/)
|
||||
and the website: https://www.fail2ban.org
|
||||
|
||||
Installation:
|
||||
@@ -33,6 +34,7 @@ this case, you should use that instead.**
|
||||
|
||||
Required:
|
||||
- [Python2 >= 2.6 or Python >= 3.2](https://www.python.org) or [PyPy](https://pypy.org)
|
||||
- python-setuptools, python-distutils or python3-setuptools for installation from source
|
||||
|
||||
Optional:
|
||||
- [pyinotify >= 0.8.3](https://github.com/seb-m/pyinotify), may require:
|
||||
@@ -45,10 +47,16 @@ Optional:
|
||||
|
||||
To install:
|
||||
|
||||
tar xvfj fail2ban-0.10.3.tar.bz2
|
||||
cd fail2ban-0.10.3
|
||||
python setup.py install
|
||||
tar xvfj fail2ban-1.0.1.tar.bz2
|
||||
cd fail2ban-1.0.1
|
||||
sudo python setup.py install
|
||||
|
||||
Alternatively, you can clone the source from GitHub to a directory of Your choice, and do the install from there. Pick the correct branch, for example, master or 0.11
|
||||
|
||||
git clone https://github.com/fail2ban/fail2ban.git
|
||||
cd fail2ban
|
||||
sudo python setup.py install
|
||||
|
||||
This will install Fail2Ban into the python library directory. The executable
|
||||
scripts are placed into `/usr/bin`, and configuration in `/etc/fail2ban`.
|
||||
|
||||
@@ -58,6 +66,9 @@ Fail2Ban should be correctly installed now. Just type:
|
||||
|
||||
to see if everything is alright. You should always use fail2ban-client and
|
||||
never call fail2ban-server directly.
|
||||
You can verify that you have the correct version installed with
|
||||
|
||||
fail2ban-client version
|
||||
|
||||
Please note that the system init/service script is not automatically installed.
|
||||
To enable fail2ban as an automatic service, simply copy the script for your
|
||||
@@ -79,11 +90,11 @@ fail2ban(1) and jail.conf(5) manpages for further references.
|
||||
Code status:
|
||||
------------
|
||||
|
||||
* [](https://travis-ci.org/fail2ban/fail2ban?branch=0.10) travis-ci.org (0.10 branch) / [](https://travis-ci.org/fail2ban/fail2ban) travis-ci.org (master branch)
|
||||
* travis-ci.org: [](https://travis-ci.org/fail2ban/fail2ban?branch=master) / [](https://travis-ci.org/fail2ban/fail2ban?branch=0.11) (0.11 branch) / [](https://travis-ci.org/fail2ban/fail2ban?branch=0.10) (0.10 branch)
|
||||
|
||||
* [](https://coveralls.io/github/fail2ban/fail2ban?branch=0.10)
|
||||
* coveralls.io: [](https://coveralls.io/github/fail2ban/fail2ban?branch=master) / [](https://coveralls.io/github/fail2ban/fail2ban?branch=0.11) (0.11 branch) / [](https://coveralls.io/github/fail2ban/fail2ban?branch=0.10) / (0.10 branch)
|
||||
|
||||
* [](https://codecov.io/gh/fail2ban/fail2ban/branch/0.10)
|
||||
* codecov.io: [](https://codecov.io/gh/fail2ban/fail2ban/branch/master) / [](https://codecov.io/gh/fail2ban/fail2ban/branch/0.11) (0.11 branch) / [](https://codecov.io/gh/fail2ban/fail2ban/branch/0.10) (0.10 branch)
|
||||
|
||||
Contact:
|
||||
--------
|
||||
|
||||
@@ -111,7 +111,7 @@ Russell Odom
|
||||
SATO Kentaro
|
||||
Sean DuBois
|
||||
Sebastian Arcus
|
||||
Serg G. Brester
|
||||
Serg G. Brester (sebres)
|
||||
Sergey Safarov
|
||||
Shaun C.
|
||||
Sireyessire
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Fail2Ban configuration file
|
||||
#
|
||||
# Author: Chris Caron <lead2gold@gmail.com>
|
||||
#
|
||||
#
|
||||
|
||||
[Definition]
|
||||
|
||||
# Option: actionstart
|
||||
# Notes.: command executed once at the start of Fail2Ban.
|
||||
# Values: CMD
|
||||
#
|
||||
actionstart = printf %%b "The jail <name> as been started successfully." | <apprise> -t "[Fail2Ban] <name>: started on `uname -n`"
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed once at the end of Fail2Ban
|
||||
# Values: CMD
|
||||
#
|
||||
actionstop = printf %%b "The jail <name> has been stopped." | <apprise> -t "[Fail2Ban] <name>: stopped on `uname -n`"
|
||||
|
||||
# Option: actioncheck
|
||||
# Notes.: command executed once before each actionban command
|
||||
# Values: CMD
|
||||
#
|
||||
actioncheck =
|
||||
|
||||
# Option: actionban
|
||||
# Notes.: command executed when banning an IP. Take care that the
|
||||
# command is executed with Fail2Ban user rights.
|
||||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionban = printf %%b "The IP <ip> has just been banned by Fail2Ban after <failures> attempts against <name>" | <apprise> -n "warning" -t "[Fail2Ban] <name>: banned <ip> from `uname -n`"
|
||||
|
||||
# Option: actionunban
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
# command is executed with Fail2Ban user rights.
|
||||
# Tags: See jail.conf(5) man page
|
||||
# Values: CMD
|
||||
#
|
||||
actionunban =
|
||||
|
||||
[Init]
|
||||
|
||||
# Define location of the default apprise configuration file to use
|
||||
#
|
||||
config = /etc/fail2ban/apprise.conf
|
||||
#
|
||||
apprise = apprise -c "<config>"
|
||||
@@ -1,19 +0,0 @@
|
||||
# Fail2ban reporting to badips.com
|
||||
#
|
||||
# Note: This reports an IP only and does not actually ban traffic. Use
|
||||
# another action in the same jail if you want bans to occur.
|
||||
#
|
||||
# Set the category to the appropriate value before use.
|
||||
#
|
||||
# To get see register and optional key to get personalised graphs see:
|
||||
# http://www.badips.com/blog/personalized-statistics-track-the-attackers-of-all-your-servers-with-one-key
|
||||
|
||||
[Definition]
|
||||
|
||||
actionban = curl --fail --user-agent "<agent>" http://www.badips.com/add/<category>/<ip>
|
||||
|
||||
[Init]
|
||||
|
||||
# Option: category
|
||||
# Notes.: Values are from the list here: http://www.badips.com/get/categories
|
||||
category =
|
||||
@@ -1,391 +0,0 @@
|
||||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import sys
|
||||
if sys.version_info < (2, 7): # pragma: no cover
|
||||
raise ImportError("badips.py action requires Python >= 2.7")
|
||||
import json
|
||||
import threading
|
||||
import logging
|
||||
if sys.version_info >= (3, ): # pragma: 2.x no cover
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.parse import urlencode
|
||||
from urllib.error import HTTPError
|
||||
else: # pragma: 3.x no cover
|
||||
from urllib2 import Request, urlopen, HTTPError
|
||||
from urllib import urlencode
|
||||
|
||||
from fail2ban.server.actions import Actions, ActionBase, BanTicket
|
||||
from fail2ban.helpers import splitwords, str2LogLevel
|
||||
|
||||
|
||||
|
||||
class BadIPsAction(ActionBase): # pragma: no cover - may be unavailable
|
||||
"""Fail2Ban action which reports bans to badips.com, and also
|
||||
blacklist bad IPs listed on badips.com by using another action's
|
||||
ban method.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jail : Jail
|
||||
The jail which the action belongs to.
|
||||
name : str
|
||||
Name assigned to the action.
|
||||
category : str
|
||||
Valid badips.com category for reporting failures.
|
||||
score : int, optional
|
||||
Minimum score for bad IPs. Default 3.
|
||||
age : str, optional
|
||||
Age of last report for bad IPs, per badips.com syntax.
|
||||
Default "24h" (24 hours)
|
||||
banaction : str, optional
|
||||
Name of banaction to use for blacklisting bad IPs. If `None`,
|
||||
no blacklist of IPs will take place.
|
||||
Default `None`.
|
||||
bancategory : str, optional
|
||||
Name of category to use for blacklisting, which can differ
|
||||
from category used for reporting. e.g. may want to report
|
||||
"postfix", but want to use whole "mail" category for blacklist.
|
||||
Default `category`.
|
||||
bankey : str, optional
|
||||
Key issued by badips.com to retrieve personal list
|
||||
of blacklist IPs.
|
||||
updateperiod : int, optional
|
||||
Time in seconds between updating bad IPs blacklist.
|
||||
Default 900 (15 minutes)
|
||||
loglevel : int/str, optional
|
||||
Log level of the message when an IP is (un)banned.
|
||||
Default `DEBUG`.
|
||||
Can be also supplied as two-value list (comma- or space separated) to
|
||||
provide level of the summary message when a group of IPs is (un)banned.
|
||||
Example `DEBUG,INFO`.
|
||||
agent : str, optional
|
||||
User agent transmitted to server.
|
||||
Default `Fail2Ban/ver.`
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If invalid `category`, `score`, `banaction` or `updateperiod`.
|
||||
"""
|
||||
|
||||
TIMEOUT = 10
|
||||
_badips = "https://www.badips.com"
|
||||
def _Request(self, url, **argv):
|
||||
return Request(url, headers={'User-Agent': self.agent}, **argv)
|
||||
|
||||
def __init__(self, jail, name, category, score=3, age="24h",
|
||||
banaction=None, bancategory=None, bankey=None, updateperiod=900,
|
||||
loglevel='DEBUG', agent="Fail2Ban", timeout=TIMEOUT):
|
||||
super(BadIPsAction, self).__init__(jail, name)
|
||||
|
||||
self.timeout = timeout
|
||||
self.agent = agent
|
||||
self.category = category
|
||||
self.score = score
|
||||
self.age = age
|
||||
self.banaction = banaction
|
||||
self.bancategory = bancategory or category
|
||||
self.bankey = bankey
|
||||
loglevel = splitwords(loglevel)
|
||||
self.sumloglevel = str2LogLevel(loglevel[-1])
|
||||
self.loglevel = str2LogLevel(loglevel[0])
|
||||
self.updateperiod = updateperiod
|
||||
|
||||
self._bannedips = set()
|
||||
# Used later for threading.Timer for updating badips
|
||||
self._timer = None
|
||||
|
||||
@staticmethod
|
||||
def isAvailable(timeout=1):
|
||||
try:
|
||||
response = urlopen(Request("/".join([BadIPsAction._badips]),
|
||||
headers={'User-Agent': "Fail2Ban"}), timeout=timeout)
|
||||
return True, ''
|
||||
except Exception as e: # pragma: no cover
|
||||
return False, e
|
||||
|
||||
def logError(self, response, what=''): # pragma: no cover - sporadical (502: Bad Gateway, etc)
|
||||
messages = {}
|
||||
try:
|
||||
messages = json.loads(response.read().decode('utf-8'))
|
||||
except:
|
||||
pass
|
||||
self._logSys.error(
|
||||
"%s. badips.com response: '%s'", what,
|
||||
messages.get('err', 'Unknown'))
|
||||
|
||||
def getCategories(self, incParents=False):
|
||||
"""Get badips.com categories.
|
||||
|
||||
Returns
|
||||
-------
|
||||
set
|
||||
Set of categories.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPError
|
||||
Any issues with badips.com request.
|
||||
ValueError
|
||||
If badips.com response didn't contain necessary information
|
||||
"""
|
||||
try:
|
||||
response = urlopen(
|
||||
self._Request("/".join([self._badips, "get", "categories"])), timeout=self.timeout)
|
||||
except HTTPError as response: # pragma: no cover
|
||||
self.logError(response, "Failed to fetch categories")
|
||||
raise
|
||||
else:
|
||||
response_json = json.loads(response.read().decode('utf-8'))
|
||||
if not 'categories' in response_json:
|
||||
err = "badips.com response lacked categories specification. Response was: %s" \
|
||||
% (response_json,)
|
||||
self._logSys.error(err)
|
||||
raise ValueError(err)
|
||||
categories = response_json['categories']
|
||||
categories_names = set(
|
||||
value['Name'] for value in categories)
|
||||
if incParents:
|
||||
categories_names.update(set(
|
||||
value['Parent'] for value in categories
|
||||
if "Parent" in value))
|
||||
return categories_names
|
||||
|
||||
def getList(self, category, score, age, key=None):
|
||||
"""Get badips.com list of bad IPs.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
category : str
|
||||
Valid badips.com category.
|
||||
score : int
|
||||
Minimum score for bad IPs.
|
||||
age : str
|
||||
Age of last report for bad IPs, per badips.com syntax.
|
||||
key : str, optional
|
||||
Key issued by badips.com to fetch IPs reported with the
|
||||
associated key.
|
||||
|
||||
Returns
|
||||
-------
|
||||
set
|
||||
Set of bad IPs.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPError
|
||||
Any issues with badips.com request.
|
||||
"""
|
||||
try:
|
||||
url = "?".join([
|
||||
"/".join([self._badips, "get", "list", category, str(score)]),
|
||||
urlencode({'age': age})])
|
||||
if key:
|
||||
url = "&".join([url, urlencode({'key': key})])
|
||||
self._logSys.debug('badips.com: get list, url: %r', url)
|
||||
response = urlopen(self._Request(url), timeout=self.timeout)
|
||||
except HTTPError as response: # pragma: no cover
|
||||
self.logError(response, "Failed to fetch bad IP list")
|
||||
raise
|
||||
else:
|
||||
return set(response.read().decode('utf-8').split())
|
||||
|
||||
@property
|
||||
def category(self):
|
||||
"""badips.com category for reporting IPs.
|
||||
"""
|
||||
return self._category
|
||||
|
||||
@category.setter
|
||||
def category(self, category):
|
||||
if category not in self.getCategories():
|
||||
self._logSys.error("Category name '%s' not valid. "
|
||||
"see badips.com for list of valid categories",
|
||||
category)
|
||||
raise ValueError("Invalid category: %s" % category)
|
||||
self._category = category
|
||||
|
||||
@property
|
||||
def bancategory(self):
|
||||
"""badips.com bancategory for fetching IPs.
|
||||
"""
|
||||
return self._bancategory
|
||||
|
||||
@bancategory.setter
|
||||
def bancategory(self, bancategory):
|
||||
if bancategory != "any" and bancategory not in self.getCategories(incParents=True):
|
||||
self._logSys.error("Category name '%s' not valid. "
|
||||
"see badips.com for list of valid categories",
|
||||
bancategory)
|
||||
raise ValueError("Invalid bancategory: %s" % bancategory)
|
||||
self._bancategory = bancategory
|
||||
|
||||
@property
|
||||
def score(self):
|
||||
"""badips.com minimum score for fetching IPs.
|
||||
"""
|
||||
return self._score
|
||||
|
||||
@score.setter
|
||||
def score(self, score):
|
||||
score = int(score)
|
||||
if 0 <= score <= 5:
|
||||
self._score = score
|
||||
else:
|
||||
raise ValueError("Score must be 0-5")
|
||||
|
||||
@property
|
||||
def banaction(self):
|
||||
"""Jail action to use for banning/unbanning.
|
||||
"""
|
||||
return self._banaction
|
||||
|
||||
@banaction.setter
|
||||
def banaction(self, banaction):
|
||||
if banaction is not None and banaction not in self._jail.actions:
|
||||
self._logSys.error("Action name '%s' not in jail '%s'",
|
||||
banaction, self._jail.name)
|
||||
raise ValueError("Invalid banaction")
|
||||
self._banaction = banaction
|
||||
|
||||
@property
|
||||
def updateperiod(self):
|
||||
"""Period in seconds between banned bad IPs will be updated.
|
||||
"""
|
||||
return self._updateperiod
|
||||
|
||||
@updateperiod.setter
|
||||
def updateperiod(self, updateperiod):
|
||||
updateperiod = int(updateperiod)
|
||||
if updateperiod > 0:
|
||||
self._updateperiod = updateperiod
|
||||
else:
|
||||
raise ValueError("Update period must be integer greater than 0")
|
||||
|
||||
def _banIPs(self, ips):
|
||||
for ip in ips:
|
||||
try:
|
||||
ai = Actions.ActionInfo(BanTicket(ip), self._jail)
|
||||
self._jail.actions[self.banaction].ban(ai)
|
||||
except Exception as e:
|
||||
self._logSys.error(
|
||||
"Error banning IP %s for jail '%s' with action '%s': %s",
|
||||
ip, self._jail.name, self.banaction, e,
|
||||
exc_info=self._logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
else:
|
||||
self._bannedips.add(ip)
|
||||
self._logSys.log(self.loglevel,
|
||||
"Banned IP %s for jail '%s' with action '%s'",
|
||||
ip, self._jail.name, self.banaction)
|
||||
|
||||
def _unbanIPs(self, ips):
|
||||
for ip in ips:
|
||||
try:
|
||||
ai = Actions.ActionInfo(BanTicket(ip), self._jail)
|
||||
self._jail.actions[self.banaction].unban(ai)
|
||||
except Exception as e:
|
||||
self._logSys.error(
|
||||
"Error unbanning IP %s for jail '%s' with action '%s': %s",
|
||||
ip, self._jail.name, self.banaction, e,
|
||||
exc_info=self._logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
else:
|
||||
self._logSys.log(self.loglevel,
|
||||
"Unbanned IP %s for jail '%s' with action '%s'",
|
||||
ip, self._jail.name, self.banaction)
|
||||
finally:
|
||||
self._bannedips.remove(ip)
|
||||
|
||||
def start(self):
|
||||
"""If `banaction` set, blacklists bad IPs.
|
||||
"""
|
||||
if self.banaction is not None:
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
"""If `banaction` set, updates blacklisted IPs.
|
||||
|
||||
Queries badips.com for list of bad IPs, removing IPs from the
|
||||
blacklist if no longer present, and adds new bad IPs to the
|
||||
blacklist.
|
||||
"""
|
||||
if self.banaction is not None:
|
||||
if self._timer:
|
||||
self._timer.cancel()
|
||||
self._timer = None
|
||||
|
||||
try:
|
||||
ips = self.getList(
|
||||
self.bancategory, self.score, self.age, self.bankey)
|
||||
# Remove old IPs no longer listed
|
||||
s = self._bannedips - ips
|
||||
m = len(s)
|
||||
self._unbanIPs(s)
|
||||
# Add new IPs which are now listed
|
||||
s = ips - self._bannedips
|
||||
p = len(s)
|
||||
self._banIPs(s)
|
||||
if m != 0 or p != 0:
|
||||
self._logSys.log(self.sumloglevel,
|
||||
"Updated IPs for jail '%s' (-%d/+%d)",
|
||||
self._jail.name, m, p)
|
||||
self._logSys.debug(
|
||||
"Next update for jail '%' in %i seconds",
|
||||
self._jail.name, self.updateperiod)
|
||||
finally:
|
||||
self._timer = threading.Timer(self.updateperiod, self.update)
|
||||
self._timer.start()
|
||||
|
||||
def stop(self):
|
||||
"""If `banaction` set, clears blacklisted IPs.
|
||||
"""
|
||||
if self.banaction is not None:
|
||||
if self._timer:
|
||||
self._timer.cancel()
|
||||
self._timer = None
|
||||
self._unbanIPs(self._bannedips.copy())
|
||||
|
||||
def ban(self, aInfo):
|
||||
"""Reports banned IP to badips.com.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
aInfo : dict
|
||||
Dictionary which includes information in relation to
|
||||
the ban.
|
||||
|
||||
Raises
|
||||
------
|
||||
HTTPError
|
||||
Any issues with badips.com request.
|
||||
"""
|
||||
try:
|
||||
url = "/".join([self._badips, "add", self.category, str(aInfo['ip'])])
|
||||
self._logSys.debug('badips.com: ban, url: %r', url)
|
||||
response = urlopen(self._Request(url), timeout=self.timeout)
|
||||
except HTTPError as response: # pragma: no cover
|
||||
self.logError(response, "Failed to ban")
|
||||
raise
|
||||
else:
|
||||
messages = json.loads(response.read().decode('utf-8'))
|
||||
self._logSys.debug(
|
||||
"Response from badips.com report: '%s'",
|
||||
messages['suc'])
|
||||
|
||||
Action = BadIPsAction
|
||||
@@ -44,7 +44,7 @@ actioncheck =
|
||||
#actionban = curl -s -o /dev/null https://www.cloudflare.com/api_json.html -d 'a=ban' -d 'tkn=<cftoken>' -d 'email=<cfuser>' -d 'key=<ip>'
|
||||
# API v4
|
||||
actionban = curl -s -o /dev/null -X POST <_cf_api_prms> \
|
||||
-d '{"mode":"block","configuration":{"target":"ip","value":"<ip>"},"notes":"Fail2Ban <name>"}' \
|
||||
-d '{"mode":"block","configuration":{"target":"<cftarget>","value":"<ip>"},"notes":"Fail2Ban <name>"}' \
|
||||
<_cf_api_url>
|
||||
|
||||
# Option: actionunban
|
||||
@@ -59,7 +59,7 @@ actionban = curl -s -o /dev/null -X POST <_cf_api_prms> \
|
||||
#actionunban = curl -s -o /dev/null https://www.cloudflare.com/api_json.html -d 'a=nul' -d 'tkn=<cftoken>' -d 'email=<cfuser>' -d 'key=<ip>'
|
||||
# API v4
|
||||
actionunban = id=$(curl -s -X GET <_cf_api_prms> \
|
||||
"<_cf_api_url>?mode=block&configuration_target=ip&configuration_value=<ip>&page=1&per_page=1¬es=Fail2Ban%%20<name>" \
|
||||
"<_cf_api_url>?mode=block&configuration_target=<cftarget>&configuration_value=<ip>&page=1&per_page=1¬es=Fail2Ban%%20<name>" \
|
||||
| { jq -r '.result[0].id' 2>/dev/null || tr -d '\n' | sed -nE 's/^.*"result"\s*:\s*\[\s*\{\s*"id"\s*:\s*"([^"]+)".*$/\1/p'; })
|
||||
if [ -z "$id" ]; then echo "<name>: id for <ip> cannot be found"; exit 0; fi;
|
||||
curl -s -o /dev/null -X DELETE <_cf_api_prms> "<_cf_api_url>/$id"
|
||||
@@ -81,3 +81,8 @@ _cf_api_prms = -H 'X-Auth-Email: <cfuser>' -H 'X-Auth-Key: <cftoken>' -H 'Conten
|
||||
cftoken =
|
||||
|
||||
cfuser =
|
||||
|
||||
cftarget = ip
|
||||
|
||||
[Init?family=inet6]
|
||||
cftarget = ip6
|
||||
|
||||
@@ -102,7 +102,7 @@ logpath = /dev/null
|
||||
# Notes.: Your system mail command. Is passed 2 args: subject and recipient
|
||||
# Values: CMD
|
||||
#
|
||||
mailcmd = mail -s
|
||||
mailcmd = mail -E 'set escape' -s
|
||||
|
||||
# Option: mailargs
|
||||
# Notes.: Additional arguments to mail command. e.g. for standard Unix mail:
|
||||
|
||||
@@ -179,7 +179,7 @@ tcpflags =
|
||||
# Notes.: Your system mail command. Is passed 2 args: subject and recipient
|
||||
# Values: CMD
|
||||
#
|
||||
mailcmd = mail -s
|
||||
mailcmd = mail -E 'set escape' -s
|
||||
|
||||
# Option: mailargs
|
||||
# Notes.: Additional arguments to mail command. e.g. for standard Unix mail:
|
||||
|
||||
@@ -18,19 +18,46 @@ before = firewallcmd-common.conf
|
||||
|
||||
[Definition]
|
||||
|
||||
actionstart = ipset create <ipmset> hash:ip timeout <default-ipsettime> <familyopt>
|
||||
actionstart = <ipstype_<ipsettype>/actionstart>
|
||||
firewall-cmd --direct --add-rule <family> filter <chain> 0 <actiontype> -m set --match-set <ipmset> src -j <blocktype>
|
||||
|
||||
actionflush = ipset flush <ipmset>
|
||||
actionflush = <ipstype_<ipsettype>/actionflush>
|
||||
|
||||
actionstop = firewall-cmd --direct --remove-rule <family> filter <chain> 0 <actiontype> -m set --match-set <ipmset> src -j <blocktype>
|
||||
<actionflush>
|
||||
ipset destroy <ipmset>
|
||||
<ipstype_<ipsettype>/actionstop>
|
||||
|
||||
actionban = <ipstype_<ipsettype>/actionban>
|
||||
|
||||
# actionprolong = %(actionban)s
|
||||
|
||||
actionunban = <ipstype_<ipsettype>/actionunban>
|
||||
|
||||
[ipstype_ipset]
|
||||
|
||||
actionstart = ipset create <ipmset> hash:ip timeout <default-ipsettime> <familyopt>
|
||||
|
||||
actionflush = ipset flush <ipmset>
|
||||
|
||||
actionstop = ipset destroy <ipmset>
|
||||
|
||||
actionban = ipset add <ipmset> <ip> timeout <ipsettime> -exist
|
||||
|
||||
actionunban = ipset del <ipmset> <ip> -exist
|
||||
|
||||
[ipstype_firewalld]
|
||||
|
||||
actionstart = firewall-cmd --direct --new-ipset=<ipmset> --type=hash:ip --option=timeout=<default-ipsettime> <firewalld_familyopt>
|
||||
|
||||
# TODO: there doesn't seem to be an explicit way to invoke the ipset flush function using firewall-cmd
|
||||
actionflush =
|
||||
|
||||
actionstop = firewall-cmd --direct --delete-ipset=<ipmset>
|
||||
|
||||
actionban = firewall-cmd --ipset=<ipmset> --add-entry=<ip>
|
||||
|
||||
actionunban = firewall-cmd --ipset=<ipmset> --remove-entry=<ip>
|
||||
|
||||
[Init]
|
||||
|
||||
# Option: chain
|
||||
@@ -54,6 +81,12 @@ ipsettime = 0
|
||||
# banaction = %(known/banaction)s[ipsettime='<timeout-bantime>']
|
||||
timeout-bantime = $([ "<bantime>" -le 2147483 ] && echo "<bantime>" || echo 0)
|
||||
|
||||
# Option: ipsettype
|
||||
# Notes.: defines type of ipset used for match-set (firewalld or ipset)
|
||||
# Values: firewalld or ipset
|
||||
# Default: ipset
|
||||
ipsettype = ipset
|
||||
|
||||
# Option: actiontype
|
||||
# Notes.: defines additions to the blocking rule
|
||||
# Values: leave empty to block all attempts from the host
|
||||
@@ -69,18 +102,20 @@ allports = -p <protocol>
|
||||
# Option: multiport
|
||||
# Notes.: addition to block access only to specific ports
|
||||
# Usage.: use in jail config: banaction = firewallcmd-ipset[actiontype=<multiport>]
|
||||
multiport = -p <protocol> -m multiport --dports "$(echo '<port>' | sed s/:/-/g)"
|
||||
multiport = -p <protocol> -m multiport --dports <port>
|
||||
|
||||
ipmset = f2b-<name>
|
||||
familyopt =
|
||||
firewalld_familyopt =
|
||||
|
||||
[Init?family=inet6]
|
||||
|
||||
ipmset = f2b-<name>6
|
||||
familyopt = family inet6
|
||||
firewalld_familyopt = --option=family=inet6
|
||||
|
||||
|
||||
# DEV NOTES:
|
||||
#
|
||||
# Author: Edgar Hoch and Daniel Black
|
||||
# Author: Edgar Hoch, Daniel Black, Sergey Brester and Mihail Politaev
|
||||
# firewallcmd-new / iptables-ipset-proto6 combined for maximium goodness
|
||||
|
||||
@@ -11,9 +11,9 @@ before = firewallcmd-common.conf
|
||||
|
||||
actionstart = firewall-cmd --direct --add-chain <family> filter f2b-<name>
|
||||
firewall-cmd --direct --add-rule <family> filter f2b-<name> 1000 -j RETURN
|
||||
firewall-cmd --direct --add-rule <family> filter <chain> 0 -m conntrack --ctstate NEW -p <protocol> -m multiport --dports "$(echo '<port>' | sed s/:/-/g)" -j f2b-<name>
|
||||
firewall-cmd --direct --add-rule <family> filter <chain> 0 -m conntrack --ctstate NEW -p <protocol> -m multiport --dports <port> -j f2b-<name>
|
||||
|
||||
actionstop = firewall-cmd --direct --remove-rule <family> filter <chain> 0 -m conntrack --ctstate NEW -p <protocol> -m multiport --dports "$(echo '<port>' | sed s/:/-/g)" -j f2b-<name>
|
||||
actionstop = firewall-cmd --direct --remove-rule <family> filter <chain> 0 -m conntrack --ctstate NEW -p <protocol> -m multiport --dports <port> -j f2b-<name>
|
||||
firewall-cmd --direct --remove-rules <family> filter f2b-<name>
|
||||
firewall-cmd --direct --remove-chain <family> filter f2b-<name>
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ before = firewallcmd-common.conf
|
||||
|
||||
actionstart = firewall-cmd --direct --add-chain <family> filter f2b-<name>
|
||||
firewall-cmd --direct --add-rule <family> filter f2b-<name> 1000 -j RETURN
|
||||
firewall-cmd --direct --add-rule <family> filter <chain> 0 -m state --state NEW -p <protocol> -m multiport --dports "$(echo '<port>' | sed s/:/-/g)" -j f2b-<name>
|
||||
firewall-cmd --direct --add-rule <family> filter <chain> 0 -m state --state NEW -p <protocol> -m multiport --dports <port> -j f2b-<name>
|
||||
|
||||
actionstop = firewall-cmd --direct --remove-rule <family> filter <chain> 0 -m state --state NEW -p <protocol> -m multiport --dports "$(echo '<port>' | sed s/:/-/g)" -j f2b-<name>
|
||||
actionstop = firewall-cmd --direct --remove-rule <family> filter <chain> 0 -m state --state NEW -p <protocol> -m multiport --dports <port> -j f2b-<name>
|
||||
firewall-cmd --direct --remove-rules <family> filter f2b-<name>
|
||||
firewall-cmd --direct --remove-chain <family> filter f2b-<name>
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ actioncheck =
|
||||
|
||||
fwcmd_rich_rule = rule family='<family>' source address='<ip>' port port='$p' protocol='<protocol>' %(rich-suffix)s
|
||||
|
||||
actionban = ports="$(echo '<port>' | sed s/:/-/g)"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --add-rich-rule="%(fwcmd_rich_rule)s"; done
|
||||
actionban = ports="<port>"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --add-rich-rule="%(fwcmd_rich_rule)s"; done
|
||||
|
||||
actionunban = ports="$(echo '<port>' | sed s/:/-/g)"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --remove-rich-rule="%(fwcmd_rich_rule)s"; done
|
||||
actionunban = ports="<port>"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --remove-rich-rule="%(fwcmd_rich_rule)s"; done
|
||||
|
||||
rich-suffix = <rich-blocktype>
|
||||
@@ -51,6 +51,8 @@ actionstop = <iptables> -D <chain> -m set --match-set <ipmset> src -j <blocktype
|
||||
#
|
||||
actionban = ipset add <ipmset> <ip> timeout <ipsettime> -exist
|
||||
|
||||
# actionprolong = %(actionban)s
|
||||
|
||||
# Option: actionunban
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
# command is executed with Fail2Ban user rights.
|
||||
|
||||
@@ -51,6 +51,8 @@ actionstop = <iptables> -D <chain> -p <protocol> -m multiport --dports <port> -m
|
||||
#
|
||||
actionban = ipset add <ipmset> <ip> timeout <ipsettime> -exist
|
||||
|
||||
# actionprolong = %(actionban)s
|
||||
|
||||
# Option: actionunban
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
# command is executed with Fail2Ban user rights.
|
||||
|
||||
@@ -17,7 +17,7 @@ actionstart = printf %%b "Hi,\n
|
||||
The jail <name> has been started successfully.\n
|
||||
Output will be buffered until <lines> lines are available.\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: started on <fq-hostname>" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: started on <fq-hostname>" <dest>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
|
||||
@@ -28,13 +28,13 @@ actionstop = if [ -f <tmpfile> ]; then
|
||||
These hosts have been banned by Fail2Ban.\n
|
||||
`cat <tmpfile>`
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: Summary from <fq-hostname>" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: Summary from <fq-hostname>" <dest>
|
||||
rm <tmpfile>
|
||||
fi
|
||||
printf %%b "Hi,\n
|
||||
The jail <name> has been stopped.\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: stopped on <fq-hostname>" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: stopped on <fq-hostname>" <dest>
|
||||
|
||||
# Option: actioncheck
|
||||
# Notes.: command executed once before each actionban command
|
||||
@@ -55,7 +55,7 @@ actionban = printf %%b "`date`: <ip> (<failures> failures)\n" >> <tmpfile>
|
||||
These hosts have been banned by Fail2Ban.\n
|
||||
`cat <tmpfile>`
|
||||
\nRegards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: Summary" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: Summary" <dest>
|
||||
rm <tmpfile>
|
||||
fi
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ actionunban =
|
||||
# Notes.: Your system mail command. Is passed 2 args: subject and recipient
|
||||
# Values: CMD
|
||||
#
|
||||
mailcmd = mail -s
|
||||
mailcmd = mail -E 'set escape' -s
|
||||
|
||||
# Default name of the chain
|
||||
#
|
||||
|
||||
@@ -20,7 +20,7 @@ norestored = 1
|
||||
actionstart = printf %%b "Hi,\n
|
||||
The jail <name> has been started successfully.\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: started on <fq-hostname>" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: started on <fq-hostname>" <dest>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
|
||||
@@ -29,7 +29,7 @@ actionstart = printf %%b "Hi,\n
|
||||
actionstop = printf %%b "Hi,\n
|
||||
The jail <name> has been stopped.\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: stopped on <fq-hostname>" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: stopped on <fq-hostname>" <dest>
|
||||
|
||||
# Option: actioncheck
|
||||
# Notes.: command executed once before each actionban command
|
||||
@@ -49,7 +49,7 @@ actionban = printf %%b "Hi,\n
|
||||
Here is more information about <ip> :\n
|
||||
`%(_whois_command)s`\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: banned <ip> from <fq-hostname>" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: banned <ip> from <fq-hostname>" <dest>
|
||||
|
||||
# Option: actionunban
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
|
||||
@@ -16,7 +16,7 @@ norestored = 1
|
||||
actionstart = printf %%b "Hi,\n
|
||||
The jail <name> has been started successfully.\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: started on <fq-hostname>" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: started on <fq-hostname>" <dest>
|
||||
|
||||
# Option: actionstop
|
||||
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
|
||||
@@ -25,7 +25,7 @@ actionstart = printf %%b "Hi,\n
|
||||
actionstop = printf %%b "Hi,\n
|
||||
The jail <name> has been stopped.\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: stopped on <fq-hostname>" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: stopped on <fq-hostname>" <dest>
|
||||
|
||||
# Option: actioncheck
|
||||
# Notes.: command executed once before each actionban command
|
||||
@@ -43,7 +43,7 @@ actionban = printf %%b "Hi,\n
|
||||
The IP <ip> has just been banned by Fail2Ban after
|
||||
<failures> attempts against <name>.\n
|
||||
Regards,\n
|
||||
Fail2Ban"|mail -s "[Fail2Ban] <name>: banned <ip> from <fq-hostname>" <dest>
|
||||
Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: banned <ip> from <fq-hostname>" <dest>
|
||||
|
||||
# Option: actionunban
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
|
||||
@@ -84,8 +84,15 @@ srv_cfg_path = /etc/nginx/
|
||||
#srv_cmd = nginx -c %(srv_cfg_path)s/nginx.conf
|
||||
srv_cmd = nginx
|
||||
|
||||
# first test configuration is correct, hereafter send reload signal:
|
||||
blck_lst_reload = %(srv_cmd)s -qt; if [ $? -eq 0 ]; then
|
||||
# pid file (used to check nginx is running):
|
||||
srv_pid = /run/nginx.pid
|
||||
|
||||
# command used to check whether nginx is running and configuration is valid:
|
||||
srv_is_running = [ -f "%(srv_pid)s" ]
|
||||
srv_check_cmd = %(srv_is_running)s && %(srv_cmd)s -qt
|
||||
|
||||
# first test nginx is running and configuration is correct, hereafter send reload signal:
|
||||
blck_lst_reload = %(srv_check_cmd)s; if [ $? -eq 0 ]; then
|
||||
%(srv_cmd)s -s reload; if [ $? -ne 0 ]; then echo 'reload failed.'; fi;
|
||||
fi;
|
||||
|
||||
|
||||
@@ -12,5 +12,5 @@ actioncheck =
|
||||
actionban = /usr/libexec/afctl -a <ip> -t <bantime>
|
||||
actionunban = /usr/libexec/afctl -r <ip>
|
||||
|
||||
[Init]
|
||||
bantime = 2880
|
||||
actionprolong = %(actionunban)s && %(actionban)s
|
||||
|
||||
|
||||
@@ -68,6 +68,8 @@ actionstop = ipset flush f2b-<name>
|
||||
#
|
||||
actionban = ipset add f2b-<name> <ip> timeout <ipsettime> -exist
|
||||
|
||||
# actionprolong = %(actionban)s
|
||||
|
||||
# Option: actionunban
|
||||
# Notes.: command executed when unbanning an IP. Take care that the
|
||||
# command is executed with Fail2Ban user rights.
|
||||
|
||||
@@ -13,16 +13,45 @@ actionstop =
|
||||
|
||||
actioncheck =
|
||||
|
||||
actionban = [ -n "<application>" ] && app="app <application>"
|
||||
ufw insert <insertpos> <blocktype> from <ip> to <destination> $app
|
||||
# ufw does "quickly process packets for which we already have a connection" in before.rules,
|
||||
# therefore all related sockets should be closed
|
||||
# actionban is using `ss` to do so, this only handles IPv4 and IPv6.
|
||||
|
||||
actionunban = [ -n "<application>" ] && app="app <application>"
|
||||
ufw delete <blocktype> from <ip> to <destination> $app
|
||||
actionban = if [ -n "<application>" ] && ufw app info "<application>"
|
||||
then
|
||||
ufw <add> <blocktype> from <ip> to <destination> app "<application>" comment "<comment>"
|
||||
else
|
||||
ufw <add> <blocktype> from <ip> to <destination> comment "<comment>"
|
||||
fi
|
||||
<kill>
|
||||
|
||||
actionunban = if [ -n "<application>" ] && ufw app info "<application>"
|
||||
then
|
||||
ufw delete <blocktype> from <ip> to <destination> app "<application>"
|
||||
else
|
||||
ufw delete <blocktype> from <ip> to <destination>
|
||||
fi
|
||||
|
||||
# Option: kill-mode
|
||||
# Notes.: can be set to ss or conntrack (may be extended later with other modes) to immediately drop all connections from banned IP, default empty (no kill)
|
||||
# Example: banaction = ufw[kill-mode=ss]
|
||||
kill-mode =
|
||||
|
||||
# intern conditional parameter used to provide killing mode after ban:
|
||||
_kill_ =
|
||||
_kill_ss = ss -K dst "[<ip>]"
|
||||
_kill_conntrack = conntrack -D -s "<ip>"
|
||||
|
||||
# Option: kill
|
||||
# Notes.: can be used to specify custom killing feature, by default depending on option kill-mode
|
||||
# Examples: banaction = ufw[kill='ss -K "( sport = :http || sport = :https )" dst "[<ip>]"']
|
||||
# banaction = ufw[kill='cutter "<ip>"']
|
||||
kill = <_kill_<kill-mode>>
|
||||
|
||||
[Init]
|
||||
# Option: insertpos
|
||||
# Notes.: The position number in the firewall list to insert the block rule
|
||||
insertpos = 1
|
||||
# Option: add
|
||||
# Notes.: can be set to "insert 1" to insert a rule at certain position (here 1):
|
||||
add = prepend
|
||||
|
||||
# Option: blocktype
|
||||
# Notes.: reject or deny
|
||||
@@ -36,6 +65,10 @@ destination = any
|
||||
# Notes.: application from sudo ufw app list
|
||||
application =
|
||||
|
||||
# Option: comment
|
||||
# Notes.: comment for rule added by fail2ban
|
||||
comment = by Fail2Ban after <failures> attempts against <name>
|
||||
|
||||
# DEV NOTES:
|
||||
#
|
||||
# Author: Guilhem Lettron
|
||||
|
||||
@@ -55,6 +55,12 @@ socket = /var/run/fail2ban/fail2ban.sock
|
||||
#
|
||||
pidfile = /var/run/fail2ban/fail2ban.pid
|
||||
|
||||
# Option: allowipv6
|
||||
# Notes.: Allows IPv6 interface:
|
||||
# Default: auto
|
||||
# Values: [ auto yes (on, true, 1) no (off, false, 0) ] Default: auto
|
||||
#allowipv6 = auto
|
||||
|
||||
# Options: dbfile
|
||||
# Notes.: Set the file for the fail2ban persistent data to be stored.
|
||||
# A value of ":memory:" means database is only stored in memory
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
[Definition]
|
||||
|
||||
failregex = ^<HOST> .*Googlebot.*$
|
||||
failregex = ^\s*<HOST> \S+ \S+(?: \S+)?\s+\S+ "[A-Z]+ /\S* [^"]*" \d+ \d+ \"[^"]*\" "[^"]*\bGooglebot/[^"]*"
|
||||
|
||||
ignoreregex =
|
||||
|
||||
datepattern = ^[^\[]*\[({DATE})
|
||||
datepattern = ^[^\[]*(\[{DATE}\s*\])
|
||||
{^LN-BEG}
|
||||
|
||||
# DEV Notes:
|
||||
|
||||
@@ -8,7 +8,7 @@ before = apache-common.conf
|
||||
|
||||
[Definition]
|
||||
|
||||
failregex = ^%(_apache_error_client)s (?:(?:AH0013[456]: )?Invalid (method|URI) in request\b|(?:AH00565: )?request failed: URI too long \(longer than \d+\)|request failed: erroneous characters after protocol string:|(?:AH00566: )?request failed: invalid characters in URI\b)
|
||||
failregex = ^%(_apache_error_client)s (?:(?:AH001[23][456]: )?Invalid (method|URI) in request\b|(?:AH00565: )?request failed: URI too long \(longer than \d+\)|request failed: erroneous characters after protocol string:|(?:AH00566: )?request failed: invalid characters in URI\b)
|
||||
|
||||
ignoreregex =
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ log_prefix= (?:NOTICE|SECURITY|WARNING)%(__pid_re)s:?(?:\[C-[\da-f]*\])?:? [^:]+
|
||||
prefregex = ^%(__prefix_line)s%(log_prefix)s <F-CONTENT>.+</F-CONTENT>$
|
||||
|
||||
failregex = ^Registration from '[^']*' failed for '<HOST>(:\d+)?' - (?:Wrong password|Username/auth name mismatch|No matching peer found|Not a local domain|Device does not match ACL|Peer is not supposed to register|ACL error \(permit/deny\)|Not a local domain)$
|
||||
^Call from '[^']*' \(<HOST>:\d+\) to extension '[^']*' rejected because extension not found in context
|
||||
^Call from '[^']*' \((?:(?:TCP|UDP):)?<HOST>:\d+\) to extension '[^']*' rejected because extension not found in context
|
||||
^(?:Host )?<HOST> (?:failed (?:to authenticate\b|MD5 authentication\b)|tried to authenticate with nonexistent user\b)
|
||||
^No registration for peer '[^']*' \(from <HOST>\)$
|
||||
^hacking attempt detected '<HOST>'$
|
||||
|
||||
@@ -8,14 +8,15 @@ before = common.conf
|
||||
[Definition]
|
||||
|
||||
_auth_worker = (?:dovecot: )?auth(?:-worker)?
|
||||
_auth_worker_info = (?:conn \w+:auth(?:-worker)? \(uid=\w+\): auth(?:-worker)?<\d+>: )?
|
||||
_daemon = (?:dovecot(?:-auth)?|auth)
|
||||
|
||||
prefregex = ^%(__prefix_line)s(?:%(_auth_worker)s(?:\([^\)]+\))?: )?(?:%(__pam_auth)s(?:\(dovecot:auth\))?: |(?:pop3|imap|managesieve|submission)-login: )?(?:Info: )?<F-CONTENT>.+</F-CONTENT>$
|
||||
prefregex = ^%(__prefix_line)s(?:%(_auth_worker)s(?:\([^\)]+\))?: )?(?:%(__pam_auth)s(?:\(dovecot:auth\))?: |(?:pop3|imap|managesieve|submission)-login: )?(?:Info: )?%(_auth_worker_info)s<F-CONTENT>.+</F-CONTENT>$
|
||||
|
||||
failregex = ^authentication failure; logname=<F-ALT_USER1>\S*</F-ALT_USER1> uid=\S* euid=\S* tty=dovecot ruser=<F-USER>\S*</F-USER> rhost=<HOST>(?:\s+user=<F-ALT_USER>\S*</F-ALT_USER>)?\s*$
|
||||
^(?:Aborted login|Disconnected|Remote closed connection|Client has quit the connection)(?::(?: [^ \(]+)+)? \((?:auth failed, \d+ attempts(?: in \d+ secs)?|tried to use (?:disabled|disallowed) \S+ auth|proxy dest auth failed)\):(?: user=<<F-USER>[^>]*</F-USER>>,)?(?: method=\S+,)? rip=<HOST>(?:[^>]*(?:, session=<\S+>)?)\s*$
|
||||
^pam\(\S+,<HOST>(?:,\S*)?\): pam_authenticate\(\) failed: (?:User not known to the underlying authentication module: \d+ Time\(s\)|Authentication failure \(password mismatch\?\)|Permission denied)\s*$
|
||||
^[a-z\-]{3,15}\(\S*,<HOST>(?:,\S*)?\): (?:unknown user|invalid credentials|Password mismatch)
|
||||
^pam\(\S+,<HOST>(?:,\S*)?\): pam_authenticate\(\) failed: (?:User not known to the underlying authentication module: \d+ Time\(s\)|Authentication failure \([Pp]assword mismatch\?\)|Permission denied)\s*$
|
||||
^[a-z\-]{3,15}\(\S*,<HOST>(?:,\S*)?\): (?:[Uu]nknown user|[Ii]nvalid credentials|[Pp]assword mismatch)
|
||||
<mdre-<mode>>
|
||||
|
||||
mdre-aggressive = ^(?:Aborted login|Disconnected|Remote closed connection|Client has quit the connection)(?::(?: [^ \(]+)+)? \((?:no auth attempts|disconnected before auth was ready,|client didn't finish \S+ auth,)(?: (?:in|waited) \d+ secs)?\):(?: user=<[^>]*>,)?(?: method=\S+,)? rip=<HOST>(?:[^>]*(?:, session=<\S+>)?)\s*$
|
||||
|
||||
@@ -14,7 +14,7 @@ before = common.conf
|
||||
|
||||
[Definition]
|
||||
|
||||
failregex = ^%(__prefix_line)s(https?:\/\/)([\da-z\.-]+)\.([a-z\.]{2,6})(\/[\w\.-]+)*\|\d{10}\|user\|<HOST>\|.+\|.+\|\d\|.*\|Login attempt failed for .+\.$
|
||||
failregex = ^%(__prefix_line)s(?:https?:\/\/)[^|]+\|[^|]+\|[^|]+\|<ADDR>\|(?:[^|]*\|)*Login attempt failed (?:for|from) <F-USER>[^|]+</F-USER>\.$
|
||||
|
||||
ignoreregex =
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ after = exim-common.local
|
||||
host_info_pre = (?:H=([\w.-]+ )?(?:\(\S+\) )?)?
|
||||
host_info_suf = (?::\d+)?(?: I=\[\S+\](:\d+)?)?(?: U=\S+)?(?: P=e?smtp)?(?: F=(?:<>|[^@]+@\S+))?\s
|
||||
host_info = %(host_info_pre)s\[<HOST>\]%(host_info_suf)s
|
||||
pid = (?: \[\d+\])?
|
||||
pid = (?: \[\d+\]| \w+ exim\[\d+\]:)?
|
||||
|
||||
# DEV Notes:
|
||||
# From exim source code: ./src/receive.c:add_host_info_for_log
|
||||
|
||||
@@ -6,24 +6,35 @@
|
||||
#
|
||||
import sys
|
||||
from fail2ban.server.ipdns import DNSUtils, IPAddr
|
||||
from threading import Thread
|
||||
|
||||
def process_args(argv):
|
||||
if len(argv) != 2:
|
||||
raise ValueError("Please provide a single IP as an argument. Got: %s\n"
|
||||
% (argv[1:]))
|
||||
if len(argv) - 1 not in (1, 2):
|
||||
raise ValueError("Usage %s ip ?timeout?. Got: %s\n"
|
||||
% (argv[0], argv[1:]))
|
||||
ip = argv[1]
|
||||
|
||||
if not IPAddr(ip).isValid:
|
||||
raise ValueError("Argument must be a single valid IP. Got: %s\n"
|
||||
% ip)
|
||||
return ip
|
||||
return argv[1:]
|
||||
|
||||
google_ips = None
|
||||
|
||||
def is_googlebot(ip):
|
||||
def is_googlebot(ip, timeout=55):
|
||||
import re
|
||||
|
||||
host = DNSUtils.ipToName(ip)
|
||||
timeout = float(timeout or 0)
|
||||
if timeout:
|
||||
def ipToNameTO(host, ip, timeout):
|
||||
host[0] = DNSUtils.ipToName(ip)
|
||||
host = [None]
|
||||
th = Thread(target=ipToNameTO, args=(host, ip, timeout)); th.daemon=True; th.start()
|
||||
th.join(timeout)
|
||||
host = host[0]
|
||||
else:
|
||||
host = DNSUtils.ipToName(ip)
|
||||
|
||||
if not host or not re.match(r'.*\.google(bot)?\.com$', host):
|
||||
return False
|
||||
host_ips = DNSUtils.dnsToIp(host)
|
||||
@@ -31,7 +42,7 @@ def is_googlebot(ip):
|
||||
|
||||
if __name__ == '__main__': # pragma: no cover
|
||||
try:
|
||||
ret = is_googlebot(process_args(sys.argv))
|
||||
ret = is_googlebot(*process_args(sys.argv))
|
||||
except ValueError as e:
|
||||
sys.stderr.write(str(e))
|
||||
sys.exit(2)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
[Definition]
|
||||
|
||||
failregex = ^: \((?:http|mod)_auth\.c\.\d+\) (?:password doesn\'t match .* username: .*|digest: auth failed for .*: wrong password|get_password failed), IP: <HOST>\s*$
|
||||
failregex = ^\s*(?:: )?\(?(?:http|mod)_auth\.c\.\d+\) (?:password doesn\'t match for (?:\S+|.*?) username:\s+<F-USER>(?:\S+|.*?)</F-USER>\s*|digest: auth failed(?: for\s+<F-ALT_USER>(?:\S+|.*?)</F-ALT_USER>\s*)?: (?:wrong password|uri mismatch \([^\)]*\))|get_password failed),? IP: <HOST>\s*$
|
||||
|
||||
ignoreregex =
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Fail2Ban filter for Monitorix (HTTP built-in server)
|
||||
#
|
||||
|
||||
[INCLUDES]
|
||||
|
||||
before = common.conf
|
||||
|
||||
[Definition]
|
||||
|
||||
_daemon = monitorix-httpd
|
||||
|
||||
# Option: failregex
|
||||
# Notes.: regex to match the password failures messages in the logfile. The
|
||||
# host must be matched by a group named "host". The tag "<HOST>" can
|
||||
# be used for standard IP/hostname matching and is only an alias for
|
||||
# (?:::f{4,6}:)?(?P<host>\S+)
|
||||
# Values: TEXT
|
||||
#
|
||||
failregex = ^(?:\s+-)?\s*(?:NOTEXIST|AUTHERR|NOTALLOWED) - <ADDR>\b
|
||||
|
||||
# Option: ignoreregex
|
||||
# Notes.: regex to ignore. If this regex matches, the line is ignored.
|
||||
# Values: TEXT
|
||||
#
|
||||
ignoreregex =
|
||||
@@ -0,0 +1,15 @@
|
||||
# Fail2Ban filter for failed MSSQL Server authentication attempts
|
||||
|
||||
[Definition]
|
||||
|
||||
failregex = ^\s*Logon\s+Login failed for user '<F-USER>(?:[^']*|.*)</F-USER>'\. [^'\[]+\[CLIENT: <ADDR>\]$
|
||||
|
||||
|
||||
# DEV Notes:
|
||||
# Tested with SQL Server 2019 on Ubuntu 18.04
|
||||
#
|
||||
# Example:
|
||||
# 2020-02-24 14:48:55.12 Logon Login failed for user 'root'. Reason: Could not find a login matching the name provided. [CLIENT: 127.0.0.1]
|
||||
#
|
||||
# Author: Rüdiger Olschewsky
|
||||
#
|
||||
@@ -32,7 +32,7 @@ __daemon_combs_re=(?:%(__pid_re)s?:\s+%(__daemon_re)s|%(__daemon_re)s%(__pid_re)
|
||||
|
||||
# hostname daemon_id spaces
|
||||
# this can be optional (for instance if we match named native log files)
|
||||
__line_prefix=(?:\s\S+ %(__daemon_combs_re)s\s+)?
|
||||
__line_prefix=(?:\s*\S+ %(__daemon_combs_re)s\s+)?
|
||||
|
||||
prefregex = ^%(__line_prefix)s(?: error:)?\s*client(?: @\S*)? <HOST>#\S+(?: \([\S.]+\))?: <F-CONTENT>.+</F-CONTENT>\s(?:denied|\(NOTAUTH\))\s*$
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Fail2Ban filter to match bad requests to nginx
|
||||
#
|
||||
|
||||
[Definition]
|
||||
|
||||
# The request often doesn't contain a method, only some encoded garbage
|
||||
# This will also match requests that are entirely empty
|
||||
failregex = ^<HOST> - \S+ \[\] "[^"]*" 400
|
||||
|
||||
datepattern = {^LN-BEG}%%ExY(?P<_sep>[-/.])%%m(?P=_sep)%%d[T ]%%H:%%M:%%S(?:[.,]%%f)?(?:\s*%%z)?
|
||||
^[^\[]*\[({DATE})
|
||||
{^LN-BEG}
|
||||
|
||||
journalmatch = _SYSTEMD_UNIT=nginx.service + _COMM=nginx
|
||||
|
||||
# Author: Jan Przybylak
|
||||
@@ -17,7 +17,9 @@ datepattern = {^LN-BEG}%%ExY(?P<_sep>[-/.])%%m(?P=_sep)%%d[T ]%%H:%%M:%%S(?:[.,]
|
||||
^[^\[]*\[({DATE})
|
||||
{^LN-BEG}
|
||||
|
||||
journalmatch = _SYSTEMD_UNIT=nginx.service + _COMM=nginx
|
||||
|
||||
# DEV Notes:
|
||||
# Based on apache-botsearch filter
|
||||
#
|
||||
# Author: Frantisek Sumsal
|
||||
# Author: Frantisek Sumsal
|
||||
|
||||
@@ -3,15 +3,32 @@
|
||||
|
||||
[Definition]
|
||||
|
||||
mode = normal
|
||||
|
||||
failregex = ^ \[error\] \d+#\d+: \*\d+ user "(?:[^"]+|.*?)":? (?:password mismatch|was not found in "[^\"]*"), client: <HOST>, server: \S*, request: "\S+ \S+ HTTP/\d+\.\d+", host: "\S+"(?:, referrer: "\S+")?\s*$
|
||||
mdre-auth = ^\s*\[error\] \d+#\d+: \*\d+ user "(?:[^"]+|.*?)":? (?:password mismatch|was not found in "[^\"]*"), client: <HOST>, server: \S*, request: "\S+ \S+ HTTP/\d+\.\d+", host: "\S+"(?:, referrer: "\S+")?\s*$
|
||||
mdre-fallback = ^\s*\[crit\] \d+#\d+: \*\d+ SSL_do_handshake\(\) failed \(SSL: error:\S+(?: \S+){1,3} too (?:long|short)\)[^,]*, client: <HOST>
|
||||
|
||||
mdre-normal = %(mdre-auth)s
|
||||
mdre-aggressive = %(mdre-auth)s
|
||||
%(mdre-fallback)s
|
||||
|
||||
failregex = <mdre-<mode>>
|
||||
|
||||
ignoreregex =
|
||||
|
||||
datepattern = {^LN-BEG}
|
||||
|
||||
journalmatch = _SYSTEMD_UNIT=nginx.service + _COMM=nginx
|
||||
|
||||
# DEV NOTES:
|
||||
# mdre-auth:
|
||||
# Based on samples in https://github.com/fail2ban/fail2ban/pull/43/files
|
||||
# Extensive search of all nginx auth failures not done yet.
|
||||
#
|
||||
# Author: Daniel Black
|
||||
|
||||
# mdre-fallback:
|
||||
# Ban people checking for TLS_FALLBACK_SCSV repeatedly
|
||||
# https://stackoverflow.com/questions/28010492/nginx-critical-error-with-ssl-handshaking/28010608#28010608
|
||||
# Author: Stephan Orlowsky
|
||||
|
||||
|
||||
@@ -44,3 +44,6 @@ failregex = ^\s*\[[a-z]+\] \d+#\d+: \*\d+ limiting requests, excess: [\d\.]+ by
|
||||
ignoreregex =
|
||||
|
||||
datepattern = {^LN-BEG}
|
||||
|
||||
journalmatch = _SYSTEMD_UNIT=nginx.service + _COMM=nginx
|
||||
|
||||
|
||||
@@ -22,10 +22,10 @@ _daemon = nsd
|
||||
# (?:::f{4,6}:)?(?P<host>[\w\-.^_]+)
|
||||
# Values: TEXT
|
||||
|
||||
failregex = ^%(__prefix_line)sinfo: ratelimit block .* query <HOST> TYPE255$
|
||||
^%(__prefix_line)sinfo: .* <HOST> refused, no acl matches\.$
|
||||
failregex = ^%(__prefix_line)sinfo: ratelimit block .* query <ADDR> TYPE255$
|
||||
^%(__prefix_line)sinfo: .* from(?: client)? <ADDR> refused, no acl matches\.?$
|
||||
|
||||
ignoreregex =
|
||||
|
||||
datepattern = {^LN-BEG}Epoch
|
||||
{^LN-BEG}
|
||||
{^LN-BEG}
|
||||
|
||||
@@ -12,16 +12,15 @@ before = common.conf
|
||||
|
||||
_daemon = postfix(-\w+)?/\w+(?:/smtp[ds])?
|
||||
_port = (?::\d+)?
|
||||
_pref = [A-Z]{4}
|
||||
|
||||
prefregex = ^%(__prefix_line)s<mdpr-<mode>> <F-CONTENT>.+</F-CONTENT>$
|
||||
|
||||
mdpr-normal = (?:\w+: reject:|(?:improper command pipelining|too many errors) after \S+)
|
||||
mdre-normal=^RCPT from [^[]*\[<HOST>\]%(_port)s: 55[04] 5\.7\.1\s
|
||||
^RCPT from [^[]*\[<HOST>\]%(_port)s: 45[04] 4\.7\.\d+ (?:Service unavailable\b|Client host rejected: cannot find your (reverse )?hostname\b)
|
||||
^RCPT from [^[]*\[<HOST>\]%(_port)s: 450 4\.7\.\d+ (<[^>]*>)?: Helo command rejected: Host not found\b
|
||||
^EHLO from [^[]*\[<HOST>\]%(_port)s: 504 5\.5\.\d+ (<[^>]*>)?: Helo command rejected: need fully-qualified hostname\b
|
||||
^(RCPT|VRFY) from [^[]*\[<HOST>\]%(_port)s: 550 5\.1\.1\s
|
||||
^RCPT from [^[]*\[<HOST>\]%(_port)s: 450 4\.1\.\d+ (<[^>]*>)?: Sender address rejected: Domain not found\b
|
||||
# Extended RE for normal mode to match reject by unknown users or undeliverable address, can be set to empty to avoid this:
|
||||
exre-user = |[Uu](?:ser unknown|ndeliverable address)
|
||||
|
||||
mdpr-normal = (?:\w+: (?:milter-)?reject:|(?:improper command pipelining|too many errors) after \S+)
|
||||
mdre-normal=^%(_pref)s from [^[]*\[<HOST>\]%(_port)s: [45][50][04] [45]\.\d\.\d+ (?:(?:<[^>]*>)?: )?(?:(?:Helo command|(?:Sender|Recipient) address) rejected: )?(?:Service unavailable|(?:Client host|Command|Data command) rejected|Relay access denied|(?:Host|Domain) not found|need fully-qualified hostname|match%(exre-user)s)\b
|
||||
^from [^[]*\[<HOST>\]%(_port)s:?
|
||||
|
||||
mdpr-auth = warning:
|
||||
@@ -31,13 +30,15 @@ mdre-auth2= ^[^[]*\[<HOST>\]%(_port)s: SASL ((?i)LOGIN|PLAIN|(?:CRAM|DIGEST)-MD5
|
||||
|
||||
# Mode "rbl" currently included in mode "normal", but if needed for jail "postfix-rbl" only:
|
||||
mdpr-rbl = %(mdpr-normal)s
|
||||
mdre-rbl = ^RCPT from [^[]*\[<HOST>\]%(_port)s: [45]54 [45]\.7\.1 Service unavailable; Client host \[\S+\] blocked\b
|
||||
mdre-rbl = ^%(_pref)s from [^[]*\[<HOST>\]%(_port)s: [45]54 [45]\.7\.1 Service unavailable; Client host \[\S+\] blocked\b
|
||||
|
||||
# Mode "rbl" currently included in mode "normal" (within 1st rule)
|
||||
mdpr-more = %(mdpr-normal)s
|
||||
mdre-more = %(mdre-normal)s
|
||||
|
||||
mdpr-ddos = (?:lost connection after(?! DATA) [A-Z]+|disconnect(?= from \S+(?: \S+=\d+)* auth=0/(?:[1-9]|\d\d+)))
|
||||
# Includes some of the log messages described in
|
||||
# <http://www.postfix.org/POSTSCREEN_README.html>.
|
||||
mdpr-ddos = (?:lost connection after(?! DATA) [A-Z]+|disconnect(?= from \S+(?: \S+=\d+)* auth=0/(?:[1-9]|\d\d+))|(?:PREGREET \d+|HANGUP) after \S+|COMMAND (?:TIME|COUNT|LENGTH) LIMIT)
|
||||
mdre-ddos = ^from [^[]*\[<HOST>\]%(_port)s:?
|
||||
|
||||
mdpr-extra = (?:%(mdpr-auth)s|%(mdpr-normal)s)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Fail2Ban filter for port scans detected by scanlogd
|
||||
|
||||
[INCLUDES]
|
||||
|
||||
# Read common prefixes. If any customizations available -- read them from
|
||||
# common.local
|
||||
before = common.conf
|
||||
|
||||
[Definition]
|
||||
|
||||
_daemon = scanlogd
|
||||
|
||||
failregex = ^%(__prefix_line)s<ADDR>(?::<F-PORT/>)? to \S+ ports\b
|
||||
|
||||
ignoreregex =
|
||||
|
||||
# Author: Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
|
||||
@@ -15,7 +15,7 @@ addr = (?:IPv6:<IP6>|<IP4>)
|
||||
prefregex = ^<F-MLFID>%(__prefix_line)s</F-MLFID><F-CONTENT>.+</F-CONTENT>$
|
||||
|
||||
failregex = ^(\S+ )?\[%(addr)s\]( \(may be forged\))?: possible SMTP attack: command=AUTH, count=\d+$
|
||||
^AUTH failure \(LOGIN\):(?: [^:]+:)? authentication failure: checkpass failed, user=<F-USER>(?:\S+|.*?)</F-USER>, relay=(?:\S+ )?\[%(addr)s\](?: \(may be forged\))?$
|
||||
^AUTH failure \([^\)]+\):(?: [^:]+:)? (?:authentication failure|user not found): [^,]*, user=<F-USER>(?:\S+|.*?)</F-USER>, relay=(?:\S+ )?\[%(addr)s\](?: \(may be forged\))?$
|
||||
ignoreregex =
|
||||
|
||||
journalmatch = _SYSTEMD_UNIT=sendmail.service
|
||||
|
||||
@@ -21,12 +21,12 @@ before = common.conf
|
||||
|
||||
_daemon = (?:(sm-(mta|acceptingconnections)|sendmail))
|
||||
__prefix_line = %(known/__prefix_line)s(?:\w{14,20}: )?
|
||||
addr = (?:IPv6:<IP6>|<IP4>)
|
||||
addr = (?:(?:IPv6:)?<IP6>|<IP4>)
|
||||
|
||||
prefregex = ^<F-MLFID>%(__prefix_line)s</F-MLFID><F-CONTENT>.+</F-CONTENT>$
|
||||
|
||||
cmnfailre = ^ruleset=check_rcpt, arg1=(?P<email><\S+@\S+>), relay=(\S+ )?\[%(addr)s\](?: \(may be forged\))?, reject=(550 5\.7\.1 (?P=email)\.\.\. Relaying denied\. (IP name possibly forged \[(\d+\.){3}\d+\]|Proper authentication required\.|IP name lookup failed \[(\d+\.){3}\d+\])|553 5\.1\.8 (?P=email)\.\.\. Domain of sender address \S+ does not exist|550 5\.[71]\.1 (?P=email)\.\.\. (Rejected: .*|User unknown))$
|
||||
^ruleset=check_relay, arg1=(?P<dom>\S+), arg2=%(addr)s, relay=((?P=dom) )?\[(\d+\.){3}\d+\](?: \(may be forged\))?, reject=421 4\.3\.2 (Connection rate limit exceeded\.|Too many open connections\.)$
|
||||
cmnfailre = ^ruleset=check_rcpt, arg1=(?P<email><\S+@\S+>), relay=(\S+ )?\[%(addr)s\](?: \(may be forged\))?, reject=(?:550 5\.7\.1(?: (?P=email)\.\.\.)?(?: Relaying denied\.)? (?:IP name possibly forged \[(\d+\.){3}\d+\]|Proper authentication required\.|IP name lookup failed \[(\d+\.){3}\d+\]|Fix reverse DNS for \S+)|553 5\.1\.8(?: (?P=email)\.\.\.)? Domain of sender address \S+ does not exist|550 5\.[71]\.1 (?P=email)\.\.\. (Rejected: .*|User unknown))$
|
||||
^ruleset=check_relay(?:, arg\d+=\S*)*, relay=(\S+ )?\[%(addr)s\](?: \(may be forged\))?, reject=421 4\.3\.2 (Connection rate limit exceeded\.|Too many open connections\.)$
|
||||
^rejecting commands from (\S* )?\[%(addr)s\] due to pre-greeting traffic after \d+ seconds$
|
||||
^(?:\S+ )?\[%(addr)s\]: (?:(?i)expn|vrfy) \S+ \[rejected\]$
|
||||
^<[^@]+@[^>]+>\.\.\. No such user here$
|
||||
|
||||
@@ -5,17 +5,23 @@ before = apache-common.conf
|
||||
|
||||
[Definition]
|
||||
|
||||
# pattern: [Wed Apr 27 23:12:07.736196 2016] [:error] [pid 2460] [client 10.1.1.1:47296] WAR [Login denied for user "test"], referer: https://zoneminderurl/index.php
|
||||
#
|
||||
# patterns: [Mon Mar 28 16:50:49.522240 2016] [:error] [pid 1795] [client 10.1.1.1:50700] WAR [Login denied for user "username1"], referer: https://zoneminder/
|
||||
# [Sun Mar 28 16:53:00.472693 2021] [php7:notice] [pid 11328] [client 10.1.1.1:39568] ERR [Could not retrieve user test details], referer: https://zm/
|
||||
# [Sun Mar 28 16:59:14.150625 2021] [php7:notice] [pid 11336] [client 10.1.1.1:39654] ERR [Login denied for user "john"], referer: https://zm/
|
||||
#
|
||||
# Option: failregex
|
||||
# Notes.: regex to match the password failure messages in the logfile.
|
||||
# Notes.: regex to match the login failure and non-existent user error messages in the logfile.
|
||||
|
||||
failregex = ^%(_apache_error_client)s WAR \[Login denied for user "[^"]*"\]
|
||||
prefregex = ^%(_apache_error_client)s (?:ERR|WAR) <F-CONTENT>\[(?:Login denied|Could not retrieve).*</F-CONTENT>$
|
||||
|
||||
failregex = ^\[Login denied for user "<F-USER>[^"]*</F-USER>"\]
|
||||
^\[Could not retrieve user <F-USER>\S*</F-USER>
|
||||
|
||||
ignoreregex =
|
||||
|
||||
# Notes:
|
||||
# Tested on Zoneminder 1.29.0
|
||||
# Tested on Zoneminder 1.29 and 1.35.21
|
||||
#
|
||||
# Zoneminder versions > 1.3x use "ERR" and < 1.3x use "WAR" level logs, so i've kept both for compatibility reasons
|
||||
#
|
||||
# Author: John Marzella
|
||||
|
||||
+71
-17
@@ -44,6 +44,44 @@ before = paths-debian.conf
|
||||
# MISCELLANEOUS OPTIONS
|
||||
#
|
||||
|
||||
# "bantime.increment" allows to use database for searching of previously banned ip's to increase a
|
||||
# default ban time using special formula, default it is banTime * 1, 2, 4, 8, 16, 32...
|
||||
#bantime.increment = true
|
||||
|
||||
# "bantime.rndtime" is the max number of seconds using for mixing with random time
|
||||
# to prevent "clever" botnets calculate exact time IP can be unbanned again:
|
||||
#bantime.rndtime =
|
||||
|
||||
# "bantime.maxtime" is the max number of seconds using the ban time can reach (doesn't grow further)
|
||||
#bantime.maxtime =
|
||||
|
||||
# "bantime.factor" is a coefficient to calculate exponent growing of the formula or common multiplier,
|
||||
# default value of factor is 1 and with default value of formula, the ban time
|
||||
# grows by 1, 2, 4, 8, 16 ...
|
||||
#bantime.factor = 1
|
||||
|
||||
# "bantime.formula" used by default to calculate next value of ban time, default value below,
|
||||
# the same ban time growing will be reached by multipliers 1, 2, 4, 8, 16, 32...
|
||||
#bantime.formula = ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor
|
||||
#
|
||||
# more aggressive example of formula has the same values only for factor "2.0 / 2.885385" :
|
||||
#bantime.formula = ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)
|
||||
|
||||
# "bantime.multipliers" used to calculate next value of ban time instead of formula, corresponding
|
||||
# previously ban count and given "bantime.factor" (for multipliers default is 1);
|
||||
# following example grows ban time by 1, 2, 4, 8, 16 ... and if last ban count greater as multipliers count,
|
||||
# always used last multiplier (64 in example), for factor '1' and original ban time 600 - 10.6 hours
|
||||
#bantime.multipliers = 1 2 4 8 16 32 64
|
||||
# following example can be used for small initial ban time (bantime=60) - it grows more aggressive at begin,
|
||||
# for bantime=60 the multipliers are minutes and equal: 1 min, 5 min, 30 min, 1 hour, 5 hour, 12 hour, 1 day, 2 day
|
||||
#bantime.multipliers = 1 5 30 60 300 720 1440 2880
|
||||
|
||||
# "bantime.overalljails" (if true) specifies the search of IP in the database will be executed
|
||||
# cross over all jails, if false (default), only current jail of the ban IP will be searched
|
||||
#bantime.overalljails = false
|
||||
|
||||
# --------------------
|
||||
|
||||
# "ignoreself" specifies whether the local resp. own IP addresses should be ignored
|
||||
# (default is true). Fail2ban will not ban a host which matches such addresses.
|
||||
#ignoreself = true
|
||||
@@ -171,7 +209,7 @@ banaction = iptables-multiport
|
||||
banaction_allports = iptables-allports
|
||||
|
||||
# The simplest action to take: ban only
|
||||
action_ = %(banaction)s[bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
|
||||
action_ = %(banaction)s[port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
|
||||
|
||||
# ban & send an e-mail with whois report to the destemail.
|
||||
action_mw = %(action_)s
|
||||
@@ -189,6 +227,15 @@ action_mwl = %(action_)s
|
||||
action_xarf = %(action_)s
|
||||
xarf-login-attack[service=%(__name__)s, sender="%(sender)s", logpath="%(logpath)s", port="%(port)s"]
|
||||
|
||||
# ban & send a notification to one or more of the 50+ services supported by Apprise.
|
||||
# See https://github.com/caronc/apprise/wiki for details on what is supported.
|
||||
#
|
||||
# You may optionally over-ride the default configuration line (containing the Apprise URLs)
|
||||
# by using 'apprise[config="/alternate/path/to/apprise.cfg"]' otherwise
|
||||
# /etc/fail2ban/apprise.conf is sourced for your supported notification configuration.
|
||||
# action = %(action_)s
|
||||
# apprise
|
||||
|
||||
# ban IP on CloudFlare & send an e-mail with whois report and relevant log lines
|
||||
# to the destemail.
|
||||
action_cf_mwl = cloudflare[cfuser="%(cfemail)s", cftoken="%(cfapikey)s"]
|
||||
@@ -204,20 +251,6 @@ action_cf_mwl = cloudflare[cfuser="%(cfemail)s", cftoken="%(cfapikey)s"]
|
||||
#
|
||||
action_blocklist_de = blocklist_de[email="%(sender)s", service="%(__name__)s", apikey="%(blocklist_de_apikey)s", agent="%(fail2ban_agent)s"]
|
||||
|
||||
# Report ban via badips.com, and use as blacklist
|
||||
#
|
||||
# See BadIPsAction docstring in config/action.d/badips.py for
|
||||
# documentation for this action.
|
||||
#
|
||||
# NOTE: This action relies on banaction being present on start and therefore
|
||||
# should be last action defined for a jail.
|
||||
#
|
||||
action_badips = badips.py[category="%(__name__)s", banaction="%(banaction)s", agent="%(fail2ban_agent)s"]
|
||||
#
|
||||
# Report ban via badips.com (uses action.d/badips.conf for reporting only)
|
||||
#
|
||||
action_badips_report = badips[category="%(__name__)s", agent="%(fail2ban_agent)s"]
|
||||
|
||||
# Report ban via abuseipdb.com.
|
||||
#
|
||||
# See action.d/abuseipdb.conf for usage example and details.
|
||||
@@ -337,8 +370,11 @@ banaction = %(banaction_allports)s
|
||||
logpath = /opt/openhab/logs/request.log
|
||||
|
||||
|
||||
# To use more aggressive http-auth modes set filter parameter "mode" in jail.local:
|
||||
# normal (default), aggressive (combines all), auth or fallback
|
||||
# See "tests/files/logs/nginx-http-auth" or "filter.d/nginx-http-auth.conf" for usage example and details.
|
||||
[nginx-http-auth]
|
||||
|
||||
# mode = normal
|
||||
port = http,https
|
||||
logpath = %(nginx_error_log)s
|
||||
|
||||
@@ -354,8 +390,10 @@ logpath = %(nginx_error_log)s
|
||||
|
||||
port = http,https
|
||||
logpath = %(nginx_error_log)s
|
||||
maxretry = 2
|
||||
|
||||
[nginx-bad-request]
|
||||
port = http,https
|
||||
logpath = %(nginx_access_log)s
|
||||
|
||||
# Ban attackers that try to use PHP's URL-fopen() functionality
|
||||
# through GET/POST variables. - Experimental, with more than a year
|
||||
@@ -759,6 +797,14 @@ logpath = %(mysql_log)s
|
||||
backend = %(mysql_backend)s
|
||||
|
||||
|
||||
[mssql-auth]
|
||||
# Default configuration for Microsoft SQL Server for Linux
|
||||
# See the 'mssql-conf' manpage how to change logpath or port
|
||||
logpath = /var/opt/mssql/log/errorlog
|
||||
port = 1433
|
||||
filter = mssql-auth
|
||||
|
||||
|
||||
# Log wrong MongoDB auth (for details see filter 'filter.d/mongodb-auth.conf')
|
||||
[mongodb-auth]
|
||||
# change port when running with "--shardsvr" or "--configsvr" runtime operation
|
||||
@@ -924,3 +970,11 @@ logpath = %(apache_error_log)s
|
||||
# see `filter.d/traefik-auth.conf` for details and service example.
|
||||
port = http,https
|
||||
logpath = /var/log/traefik/access.log
|
||||
|
||||
[scanlogd]
|
||||
logpath = %(syslog_local0)s
|
||||
banaction = %(banaction_allports)s
|
||||
|
||||
[monitorix]
|
||||
port = 8080
|
||||
logpath = /var/log/monitorix-httpd
|
||||
|
||||
@@ -26,3 +26,5 @@ exim_main_log = /var/log/exim4/mainlog
|
||||
# was in debian squeezy but not in wheezy
|
||||
# /etc/proftpd/proftpd.conf (SystemLog)
|
||||
proftpd_log = /var/log/proftpd/proftpd.log
|
||||
|
||||
roundcube_errors_log = /var/log/roundcube/errors.log
|
||||
|
||||
@@ -46,6 +46,7 @@ class ActionReader(DefinitionInitConfigReader):
|
||||
"actionrepair": ["string", None],
|
||||
"actionrepair_on_unban": ["bool", None],
|
||||
"actionban": ["string", None],
|
||||
"actionprolong": ["string", None],
|
||||
"actionreban": ["string", None],
|
||||
"actionunban": ["string", None],
|
||||
"norestored": ["bool", None],
|
||||
|
||||
@@ -180,6 +180,12 @@ class Beautifier:
|
||||
msg = "The jail %s action %s has the following " \
|
||||
"methods:\n" % (inC[1], inC[3])
|
||||
msg += ", ".join(response)
|
||||
elif inC[2] == "banip" and inC[0] == "get":
|
||||
if isinstance(response, list):
|
||||
sep = " " if len(inC) <= 3 else inC[3]
|
||||
if sep == "--with-time":
|
||||
sep = "\n"
|
||||
msg = sep.join(response)
|
||||
except Exception:
|
||||
logSys.warning("Beautifier error. Please report the error")
|
||||
logSys.error("Beautify %r with %r failed", response, self.__inputCmd,
|
||||
|
||||
@@ -230,7 +230,7 @@ class Fail2banClient(Fail2banCmdLine, Thread):
|
||||
logSys.log(5, ' client phase %s', phase)
|
||||
if not stream:
|
||||
return False
|
||||
# wait a litle bit for phase "start-ready" before enter active waiting:
|
||||
# wait a little bit for phase "start-ready" before enter active waiting:
|
||||
if phase is not None:
|
||||
Utils.wait_for(lambda: phase.get('start-ready', None) is not None, 0.5, 0.001)
|
||||
phase['configure'] = (True if stream else False)
|
||||
|
||||
@@ -192,7 +192,7 @@ class Fail2banCmdLine():
|
||||
cmdOpts = 'hc:s:p:xfbdtviqV'
|
||||
cmdLongOpts = ['loglevel=', 'logtarget=', 'syslogsocket=', 'test', 'async',
|
||||
'conf=', 'pidfile=', 'pname=', 'socket=',
|
||||
'timeout=', 'str2sec=', 'help', 'version', 'dp', '--dump-pretty']
|
||||
'timeout=', 'str2sec=', 'help', 'version', 'dp', 'dump-pretty']
|
||||
optList, self._args = getopt.getopt(self._argv[1:], cmdOpts, cmdLongOpts)
|
||||
except getopt.GetoptError:
|
||||
self.dispUsage()
|
||||
|
||||
@@ -53,6 +53,7 @@ class Fail2banReader(ConfigReader):
|
||||
opts = [["string", "loglevel", "INFO" ],
|
||||
["string", "logtarget", "STDERR"],
|
||||
["string", "syslogsocket", "auto"],
|
||||
["string", "allowipv6", "auto"],
|
||||
["string", "dbfile", "/var/lib/fail2ban/fail2ban.sqlite3"],
|
||||
["int", "dbmaxmatches", None],
|
||||
["string", "dbpurgeage", "1d"]]
|
||||
@@ -74,6 +75,7 @@ class Fail2banReader(ConfigReader):
|
||||
# Also dbfile should be set before all other database options.
|
||||
# So adding order indices into items, to be stripped after sorting, upon return
|
||||
order = {"thread":0, "syslogsocket":11, "loglevel":12, "logtarget":13,
|
||||
"allowipv6": 14,
|
||||
"dbfile":50, "dbmaxmatches":51, "dbpurgeage":51}
|
||||
stream = list()
|
||||
for opt in self.__opts:
|
||||
|
||||
@@ -35,11 +35,11 @@ __license__ = "GPL"
|
||||
|
||||
import getopt
|
||||
import logging
|
||||
import re
|
||||
import os
|
||||
import shlex
|
||||
import sys
|
||||
import time
|
||||
import time
|
||||
import urllib
|
||||
from optparse import OptionParser, Option
|
||||
|
||||
@@ -52,7 +52,7 @@ except ImportError:
|
||||
|
||||
from ..version import version, normVersion
|
||||
from .filterreader import FilterReader
|
||||
from ..server.filter import Filter, FileContainer
|
||||
from ..server.filter import Filter, FileContainer, MyTime
|
||||
from ..server.failregex import Regex, RegexException
|
||||
|
||||
from ..helpers import str2LogLevel, getVerbosityFormat, FormatterWithTraceBack, getLogger, \
|
||||
@@ -269,15 +269,19 @@ class Fail2banRegex(object):
|
||||
self.setJournalMatch(shlex.split(opts.journalmatch))
|
||||
if opts.timezone:
|
||||
self._filter.setLogTimeZone(opts.timezone)
|
||||
self._filter.checkFindTime = False
|
||||
if True: # not opts.out:
|
||||
MyTime.setAlternateNow(0); # accept every date (years from 19xx up to end of current century, '%ExY' and 'Exy' patterns)
|
||||
from ..server.strptime import _updateTimeRE
|
||||
_updateTimeRE()
|
||||
if opts.datepattern:
|
||||
self.setDatePattern(opts.datepattern)
|
||||
if opts.usedns:
|
||||
self._filter.setUseDns(opts.usedns)
|
||||
self._filter.returnRawHost = opts.raw
|
||||
self._filter.checkFindTime = False
|
||||
self._filter.checkAllRegex = opts.checkAllRegex and not opts.out
|
||||
# ignore pending (without ID/IP), added to matches if it hits later (if ID/IP can be retreved)
|
||||
self._filter.ignorePending = opts.out
|
||||
self._filter.ignorePending = bool(opts.out)
|
||||
# callback to increment ignored RE's by index (during process):
|
||||
self._filter.onIgnoreRegex = self._onIgnoreRegex
|
||||
self._backend = 'auto'
|
||||
@@ -285,9 +289,6 @@ class Fail2banRegex(object):
|
||||
def output(self, line):
|
||||
if not self._opts.out: output(line)
|
||||
|
||||
def decode_line(self, line):
|
||||
return FileContainer.decode_line('<LOG>', self._encoding, line)
|
||||
|
||||
def encode_line(self, line):
|
||||
return line.encode(self._encoding, 'ignore')
|
||||
|
||||
@@ -326,26 +327,33 @@ class Fail2banRegex(object):
|
||||
regex = regextype + 'regex'
|
||||
# try to check - we've case filter?[options...]?:
|
||||
basedir = self._opts.config
|
||||
fltName = value
|
||||
fltFile = None
|
||||
fltOpt = {}
|
||||
if regextype == 'fail':
|
||||
fltName, fltOpt = extractOptions(value)
|
||||
if fltName is not None:
|
||||
if "." in fltName[~5:]:
|
||||
tryNames = (fltName,)
|
||||
else:
|
||||
tryNames = (fltName, fltName + '.conf', fltName + '.local')
|
||||
for fltFile in tryNames:
|
||||
if not "/" in fltFile:
|
||||
if os.path.basename(basedir) == 'filter.d':
|
||||
fltFile = os.path.join(basedir, fltFile)
|
||||
else:
|
||||
fltFile = os.path.join(basedir, 'filter.d', fltFile)
|
||||
if re.search(r'^/{0,3}[\w/_\-.]+(?:\[.*\])?$', value):
|
||||
try:
|
||||
fltName, fltOpt = extractOptions(value)
|
||||
if "." in fltName[~5:]:
|
||||
tryNames = (fltName,)
|
||||
else:
|
||||
basedir = os.path.dirname(fltFile)
|
||||
if os.path.isfile(fltFile):
|
||||
break
|
||||
fltFile = None
|
||||
tryNames = (fltName, fltName + '.conf', fltName + '.local')
|
||||
for fltFile in tryNames:
|
||||
if not "/" in fltFile:
|
||||
if os.path.basename(basedir) == 'filter.d':
|
||||
fltFile = os.path.join(basedir, fltFile)
|
||||
else:
|
||||
fltFile = os.path.join(basedir, 'filter.d', fltFile)
|
||||
else:
|
||||
basedir = os.path.dirname(fltFile)
|
||||
if os.path.isfile(fltFile):
|
||||
break
|
||||
fltFile = None
|
||||
except Exception as e:
|
||||
output("ERROR: Wrong filter name or options: %s" % (str(e),))
|
||||
output(" while parsing: %s" % (value,))
|
||||
if self._verbose: raise(e)
|
||||
return False
|
||||
# if it is filter file:
|
||||
if fltFile is not None:
|
||||
if (basedir == self._opts.config
|
||||
@@ -712,10 +720,6 @@ class Fail2banRegex(object):
|
||||
|
||||
return True
|
||||
|
||||
def file_lines_gen(self, hdlr):
|
||||
for line in hdlr:
|
||||
yield self.decode_line(line)
|
||||
|
||||
def start(self, args):
|
||||
|
||||
cmd_log, cmd_regex = args[:2]
|
||||
@@ -734,10 +738,10 @@ class Fail2banRegex(object):
|
||||
|
||||
if os.path.isfile(cmd_log):
|
||||
try:
|
||||
hdlr = open(cmd_log, 'rb')
|
||||
test_lines = FileContainer(cmd_log, self._encoding, doOpen=True)
|
||||
|
||||
self.output( "Use log file : %s" % cmd_log )
|
||||
self.output( "Use encoding : %s" % self._encoding )
|
||||
test_lines = self.file_lines_gen(hdlr)
|
||||
except IOError as e: # pragma: no cover
|
||||
output( e )
|
||||
return False
|
||||
|
||||
@@ -98,6 +98,13 @@ class JailReader(ConfigReader):
|
||||
"maxmatches": ["int", None],
|
||||
"findtime": ["string", None],
|
||||
"bantime": ["string", None],
|
||||
"bantime.increment": ["bool", None],
|
||||
"bantime.factor": ["string", None],
|
||||
"bantime.formula": ["string", None],
|
||||
"bantime.multipliers": ["string", None],
|
||||
"bantime.maxtime": ["string", None],
|
||||
"bantime.rndtime": ["string", None],
|
||||
"bantime.overalljails": ["bool", None],
|
||||
"ignorecommand": ["string", None],
|
||||
"ignoreself": ["bool", None],
|
||||
"ignoreip": ["string", None],
|
||||
@@ -133,9 +140,10 @@ class JailReader(ConfigReader):
|
||||
# Read filter
|
||||
flt = self.__opts["filter"]
|
||||
if flt:
|
||||
filterName, filterOpt = extractOptions(flt)
|
||||
if not filterName:
|
||||
raise JailDefError("Invalid filter definition %r" % flt)
|
||||
try:
|
||||
filterName, filterOpt = extractOptions(flt)
|
||||
except ValueError as e:
|
||||
raise JailDefError("Invalid filter definition %r: %s" % (flt, e))
|
||||
self.__filter = FilterReader(
|
||||
filterName, self.__name, filterOpt,
|
||||
share_config=self.share_config, basedir=self.getBaseDir())
|
||||
@@ -167,10 +175,10 @@ class JailReader(ConfigReader):
|
||||
if not act: # skip empty actions
|
||||
continue
|
||||
# join with previous line if needed (consider possible new-line):
|
||||
actName, actOpt = extractOptions(act)
|
||||
prevln = ''
|
||||
if not actName:
|
||||
raise JailDefError("Invalid action definition %r" % act)
|
||||
try:
|
||||
actName, actOpt = extractOptions(act)
|
||||
except ValueError as e:
|
||||
raise JailDefError("Invalid action definition %r: %s" % (act, e))
|
||||
if actName.endswith(".py"):
|
||||
self.__actions.append([
|
||||
"set",
|
||||
|
||||
+9
-3
@@ -371,7 +371,7 @@ OPTION_CRE = re.compile(r"^([^\[]+)(?:\[(.*)\])?\s*$", re.DOTALL)
|
||||
# since v0.10 separator extended with `]\s*[` for support of multiple option groups, syntax
|
||||
# `action = act[p1=...][p2=...]`
|
||||
OPTION_EXTRACT_CRE = re.compile(
|
||||
r'([\w\-_\.]+)=(?:"([^"]*)"|\'([^\']*)\'|([^,\]]*))(?:,|\]\s*\[|$)', re.DOTALL)
|
||||
r'\s*([\w\-_\.]+)=(?:"([^"]*)"|\'([^\']*)\'|([^,\]]*))(?:,|\]\s*\[|$|(?P<wrngA>.+))|,?\s*$|(?P<wrngB>.+)', re.DOTALL)
|
||||
# split by new-line considering possible new-lines within options [...]:
|
||||
OPTION_SPLIT_CRE = re.compile(
|
||||
r'(?:[^\[\s]+(?:\s*\[\s*(?:[\w\-_\.]+=(?:"[^"]*"|\'[^\']*\'|[^,\]]*)\s*(?:,|\]\s*\[)?\s*)*\])?\s*|\S+)(?=\n\s*|\s+|$)', re.DOTALL)
|
||||
@@ -379,13 +379,19 @@ OPTION_SPLIT_CRE = re.compile(
|
||||
def extractOptions(option):
|
||||
match = OPTION_CRE.match(option)
|
||||
if not match:
|
||||
# TODO proper error handling
|
||||
return None, None
|
||||
raise ValueError("unexpected option syntax")
|
||||
option_name, optstr = match.groups()
|
||||
option_opts = dict()
|
||||
if optstr:
|
||||
for optmatch in OPTION_EXTRACT_CRE.finditer(optstr):
|
||||
if optmatch.group("wrngA"):
|
||||
raise ValueError("unexpected syntax at %d after option %r: %s" % (
|
||||
optmatch.start("wrngA"), optmatch.group(1), optmatch.group("wrngA")[0:25]))
|
||||
if optmatch.group("wrngB"):
|
||||
raise ValueError("expected option, wrong syntax at %d: %s" % (
|
||||
optmatch.start("wrngB"), optmatch.group("wrngB")[0:25]))
|
||||
opt = optmatch.group(1)
|
||||
if not opt: continue
|
||||
value = [
|
||||
val for val in optmatch.group(2,3,4) if val is not None][0]
|
||||
option_opts[opt.strip()] = value.strip()
|
||||
|
||||
@@ -134,8 +134,9 @@ protocol = [
|
||||
["get <JAIL> ignoreregex", "gets the list of regular expressions which matches patterns to ignore for <JAIL>"],
|
||||
["get <JAIL> findtime", "gets the time for which the filter will look back for failures for <JAIL>"],
|
||||
["get <JAIL> bantime", "gets the time a host is banned for <JAIL>"],
|
||||
["get <JAIL> datepattern", "gets the patern used to match date/times for <JAIL>"],
|
||||
["get <JAIL> datepattern", "gets the pattern used to match date/times for <JAIL>"],
|
||||
["get <JAIL> usedns", "gets the usedns setting for <JAIL>"],
|
||||
["get <JAIL> banip [<SEP>|--with-time]", "gets the list of of banned IP addresses for <JAIL>. Optionally the separator character ('<SEP>', default is space) or the option '--with-time' (printing the times of ban) may be specified. The IPs are ordered by end of ban."],
|
||||
["get <JAIL> maxretry", "gets the number of failures allowed for <JAIL>"],
|
||||
["get <JAIL> maxmatches", "gets the max number of matches stored in memory per ticket in <JAIL>"],
|
||||
["get <JAIL> maxlines", "gets the number of lines to buffer for <JAIL>"],
|
||||
|
||||
+92
-33
@@ -30,7 +30,10 @@ import tempfile
|
||||
import threading
|
||||
import time
|
||||
from abc import ABCMeta
|
||||
from collections import MutableMapping
|
||||
try:
|
||||
from collections.abc import MutableMapping
|
||||
except ImportError:
|
||||
from collections import MutableMapping
|
||||
|
||||
from .failregex import mapTag2Opt
|
||||
from .ipdns import DNSUtils
|
||||
@@ -262,6 +265,10 @@ class ActionBase(object):
|
||||
"""
|
||||
return self.ban(aInfo)
|
||||
|
||||
@property
|
||||
def _prolongable(self): # pragma: no cover - abstract
|
||||
return False
|
||||
|
||||
def unban(self, aInfo): # pragma: no cover - abstract
|
||||
"""Executed when a ban expires.
|
||||
|
||||
@@ -274,6 +281,11 @@ class ActionBase(object):
|
||||
pass
|
||||
|
||||
|
||||
WRAP_CMD_PARAMS = {
|
||||
'timeout': 'str2seconds',
|
||||
'bantime': 'ignore',
|
||||
}
|
||||
|
||||
class CommandAction(ActionBase):
|
||||
"""A action which executes OS shell commands.
|
||||
|
||||
@@ -346,7 +358,10 @@ class CommandAction(ActionBase):
|
||||
def __setattr__(self, name, value):
|
||||
if not name.startswith('_') and not self.__init and not callable(value):
|
||||
# special case for some parameters:
|
||||
if name in ('timeout', 'bantime'):
|
||||
wrp = WRAP_CMD_PARAMS.get(name)
|
||||
if wrp == 'ignore': # ignore (filter) dynamic parameters
|
||||
return
|
||||
elif wrp == 'str2seconds':
|
||||
value = MyTime.str2seconds(value)
|
||||
# parameters changed - clear properties and substitution cache:
|
||||
self.__properties = None
|
||||
@@ -395,7 +410,7 @@ class CommandAction(ActionBase):
|
||||
cmd = self.replaceTag(tag, self._properties,
|
||||
conditional=('family='+family if family else ''),
|
||||
cache=self.__substCache)
|
||||
if '<' not in cmd or not family: return cmd
|
||||
if not family or '<' not in cmd: return cmd
|
||||
# replace family as dynamic tags, important - don't cache, no recursion and auto-escape here:
|
||||
cmd = self.replaceDynamicTags(cmd, {'family':family})
|
||||
return cmd
|
||||
@@ -443,7 +458,18 @@ class CommandAction(ActionBase):
|
||||
ret = True
|
||||
# avoid double execution of same command for both families:
|
||||
if cmd and cmd not in self._operationExecuted(tag, lambda f: f != famoper):
|
||||
ret = self.executeCmd(cmd, self.timeout)
|
||||
realCmd = cmd
|
||||
if self._jail:
|
||||
# simulate action info with "empty" ticket:
|
||||
aInfo = getattr(self._jail.actions, 'actionInfo', None)
|
||||
if not aInfo:
|
||||
aInfo = self._jail.actions._getActionInfo(None)
|
||||
setattr(self._jail.actions, 'actionInfo', aInfo)
|
||||
aInfo['time'] = MyTime.time()
|
||||
aInfo['family'] = famoper
|
||||
# replace dynamical tags, important - don't cache, no recursion and auto-escape here
|
||||
realCmd = self.replaceDynamicTags(cmd, aInfo)
|
||||
ret = self.executeCmd(realCmd, self.timeout)
|
||||
res &= ret
|
||||
if afterExec: afterExec(famoper, ret)
|
||||
self._operationExecuted(tag, famoper, cmd if ret else None)
|
||||
@@ -543,6 +569,26 @@ class CommandAction(ActionBase):
|
||||
raise RuntimeError("Error banning %(ip)s" % aInfo)
|
||||
self.__started[family] = self.__started.get(family, 0) | 3; # started and contains items
|
||||
|
||||
@property
|
||||
def _prolongable(self):
|
||||
return (hasattr(self, 'actionprolong') and self.actionprolong
|
||||
and not str(self.actionprolong).isspace())
|
||||
|
||||
def prolong(self, aInfo):
|
||||
"""Executes the "actionprolong" command.
|
||||
|
||||
Replaces the tags in the action command with actions properties
|
||||
and ban information, and executes the resulting command.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
aInfo : dict
|
||||
Dictionary which includes information in relation to
|
||||
the ban.
|
||||
"""
|
||||
if not self._processCmd('<actionprolong>', aInfo):
|
||||
raise RuntimeError("Error prolonging %(ip)s" % aInfo)
|
||||
|
||||
def unban(self, aInfo):
|
||||
"""Executes the "actionunban" command.
|
||||
|
||||
@@ -652,8 +698,10 @@ class CommandAction(ActionBase):
|
||||
ret &= False
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def escapeTag(value):
|
||||
ESCAPE_CRE = re.compile(r"""[\\#&;`|*?~<>^()\[\]{}$'"\n\r]""")
|
||||
|
||||
@classmethod
|
||||
def escapeTag(cls, value):
|
||||
"""Escape characters which may be used for command injection.
|
||||
|
||||
Parameters
|
||||
@@ -670,12 +718,15 @@ class CommandAction(ActionBase):
|
||||
-----
|
||||
The following characters are escaped::
|
||||
|
||||
\\#&;`|*?~<>^()[]{}$'"
|
||||
\\#&;`|*?~<>^()[]{}$'"\n\r
|
||||
|
||||
"""
|
||||
for c in '\\#&;`|*?~<>^()[]{}$\'"':
|
||||
if c in value:
|
||||
value = value.replace(c, '\\' + c)
|
||||
_map2c = {'\n': 'n', '\r': 'r'}
|
||||
def substChar(m):
|
||||
c = m.group()
|
||||
return '\\' + _map2c.get(c, c)
|
||||
|
||||
value = cls.ESCAPE_CRE.sub(substChar, value)
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
@@ -831,7 +882,7 @@ class CommandAction(ActionBase):
|
||||
tickData = aInfo.get("F-*")
|
||||
if not tickData: tickData = {}
|
||||
def substTag(m):
|
||||
tag = mapTag2Opt(m.groups()[0])
|
||||
tag = mapTag2Opt(m.group(1))
|
||||
try:
|
||||
value = uni_string(tickData[tag])
|
||||
except KeyError:
|
||||
@@ -926,31 +977,38 @@ class CommandAction(ActionBase):
|
||||
except (KeyError, TypeError):
|
||||
family = ''
|
||||
|
||||
# invariant check:
|
||||
if self.actioncheck:
|
||||
# don't repair/restore if unban (no matter):
|
||||
def _beforeRepair():
|
||||
if cmd == '<actionunban>' and not self._properties.get('actionrepair_on_unban'):
|
||||
self._logSys.error("Invariant check failed. Unban is impossible.")
|
||||
repcnt = 0
|
||||
while True:
|
||||
|
||||
# got some error, do invariant check:
|
||||
if repcnt and self.actioncheck:
|
||||
# don't repair/restore if unban (no matter):
|
||||
def _beforeRepair():
|
||||
if cmd == '<actionunban>' and not self._properties.get('actionrepair_on_unban'):
|
||||
self._logSys.error("Invariant check failed. Unban is impossible.")
|
||||
return False
|
||||
return True
|
||||
# check and repair if broken:
|
||||
ret = self._invariantCheck(family, _beforeRepair, forceStart=(cmd != '<actionunban>'))
|
||||
# if not sane (and not restored) return:
|
||||
if ret != 1:
|
||||
return False
|
||||
return True
|
||||
# check and repair if broken:
|
||||
ret = self._invariantCheck(family, _beforeRepair, forceStart=(cmd != '<actionunban>'))
|
||||
# if not sane (and not restored) return:
|
||||
if ret != 1:
|
||||
return False
|
||||
|
||||
# Replace static fields
|
||||
realCmd = self.replaceTag(cmd, self._properties,
|
||||
conditional=('family='+family if family else ''), cache=self.__substCache)
|
||||
# Replace static fields
|
||||
realCmd = self.replaceTag(cmd, self._properties,
|
||||
conditional=('family='+family if family else ''), cache=self.__substCache)
|
||||
|
||||
# Replace dynamical tags, important - don't cache, no recursion and auto-escape here
|
||||
if aInfo is not None:
|
||||
realCmd = self.replaceDynamicTags(realCmd, aInfo)
|
||||
else:
|
||||
realCmd = cmd
|
||||
# Replace dynamical tags, important - don't cache, no recursion and auto-escape here
|
||||
if aInfo is not None:
|
||||
realCmd = self.replaceDynamicTags(realCmd, aInfo)
|
||||
else:
|
||||
realCmd = cmd
|
||||
|
||||
return self.executeCmd(realCmd, self.timeout)
|
||||
# try execute command:
|
||||
ret = self.executeCmd(realCmd, self.timeout)
|
||||
repcnt += 1
|
||||
if ret or repcnt > 1:
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def executeCmd(realCmd, timeout=60, **kwargs):
|
||||
@@ -975,7 +1033,8 @@ class CommandAction(ActionBase):
|
||||
RuntimeError
|
||||
If command execution times out.
|
||||
"""
|
||||
logSys.debug(realCmd)
|
||||
if logSys.getEffectiveLevel() < logging.DEBUG:
|
||||
logSys.log(9, realCmd)
|
||||
if not realCmd:
|
||||
logSys.debug("Nothing to do")
|
||||
return True
|
||||
|
||||
+91
-35
@@ -28,7 +28,10 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from collections import Mapping
|
||||
try:
|
||||
from collections.abc import Mapping
|
||||
except ImportError:
|
||||
from collections import Mapping
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
@@ -39,6 +42,7 @@ from .ipdns import IPAddr
|
||||
from .jailthread import JailThread
|
||||
from .action import ActionBase, CommandAction, CallingMap
|
||||
from .mytime import MyTime
|
||||
from .observer import Observers
|
||||
from .utils import Utils
|
||||
from ..helpers import getLogger
|
||||
|
||||
@@ -80,7 +84,7 @@ class Actions(JailThread, Mapping):
|
||||
self._jail = jail
|
||||
self._actions = OrderedDict()
|
||||
## The ban manager.
|
||||
self.__banManager = BanManager()
|
||||
self.banManager = BanManager()
|
||||
self.banEpoch = 0
|
||||
self.__lastConsistencyCheckTM = 0
|
||||
## Precedence of ban (over unban), so max number of tickets banned (to call an unban check):
|
||||
@@ -199,7 +203,7 @@ class Actions(JailThread, Mapping):
|
||||
|
||||
def setBanTime(self, value):
|
||||
value = MyTime.str2seconds(value)
|
||||
self.__banManager.setBanTime(value)
|
||||
self.banManager.setBanTime(value)
|
||||
logSys.info(" banTime: %s" % value)
|
||||
|
||||
##
|
||||
@@ -208,16 +212,26 @@ class Actions(JailThread, Mapping):
|
||||
# @return the time
|
||||
|
||||
def getBanTime(self):
|
||||
return self.__banManager.getBanTime()
|
||||
return self.banManager.getBanTime()
|
||||
|
||||
def getBanned(self, ids):
|
||||
lst = self.__banManager.getBanList()
|
||||
lst = self.banManager.getBanList()
|
||||
if not ids:
|
||||
return lst
|
||||
if len(ids) == 1:
|
||||
return 1 if ids[0] in lst else 0
|
||||
return map(lambda ip: 1 if ip in lst else 0, ids)
|
||||
|
||||
def getBanList(self, withTime=False):
|
||||
"""Returns the list of banned IP addresses.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
The list of banned IP addresses.
|
||||
"""
|
||||
return self.banManager.getBanList(ordered=True, withTime=withTime)
|
||||
|
||||
def addBannedIP(self, ip):
|
||||
"""Ban an IP or list of IPs."""
|
||||
unixTime = MyTime.time()
|
||||
@@ -268,7 +282,7 @@ class Actions(JailThread, Mapping):
|
||||
if db and self._jail.database is not None:
|
||||
self._jail.database.delBan(self._jail, ip)
|
||||
# Find the ticket with the IP.
|
||||
ticket = self.__banManager.getTicketByID(ip)
|
||||
ticket = self.banManager.getTicketByID(ip)
|
||||
if ticket is not None:
|
||||
# Unban the IP.
|
||||
self.__unBan(ticket)
|
||||
@@ -277,7 +291,7 @@ class Actions(JailThread, Mapping):
|
||||
if not isinstance(ip, IPAddr):
|
||||
ipa = IPAddr(ip)
|
||||
if not ipa.isSingle: # subnet (mask/cidr) or raw (may be dns/hostname):
|
||||
ips = filter(ipa.contains, self.__banManager.getBanList())
|
||||
ips = filter(ipa.contains, self.banManager.getBanList())
|
||||
if ips:
|
||||
return self.removeBannedIP(ips, db, ifexists)
|
||||
# not found:
|
||||
@@ -336,7 +350,7 @@ class Actions(JailThread, Mapping):
|
||||
continue
|
||||
# wait for ban (stop if gets inactive, pending ban or unban):
|
||||
bancnt = 0
|
||||
wt = min(self.sleeptime, self.__banManager._nextUnbanTime - MyTime.time())
|
||||
wt = min(self.sleeptime, self.banManager._nextUnbanTime - MyTime.time())
|
||||
logSys.log(5, "Actions: wait for pending tickets %s (default %s)", wt, self.sleeptime)
|
||||
if Utils.wait_for(lambda: not self.active or self._jail.hasFailTickets, wt):
|
||||
bancnt = self.__checkBan()
|
||||
@@ -370,6 +384,8 @@ class Actions(JailThread, Mapping):
|
||||
"fid": lambda self: self.__ticket.getID(),
|
||||
"failures": lambda self: self.__ticket.getAttempt(),
|
||||
"time": lambda self: self.__ticket.getTime(),
|
||||
"bantime": lambda self: self._getBanTime(),
|
||||
"bancount": lambda self: self.__ticket.getBanCount(),
|
||||
"matches": lambda self: "\n".join(self.__ticket.getMatches()),
|
||||
# to bypass actions, that should not be executed for restored tickets
|
||||
"restored": lambda self: (1 if self.__ticket.restored else 0),
|
||||
@@ -381,7 +397,12 @@ class Actions(JailThread, Mapping):
|
||||
"ipfailures": lambda self: self._mi4ip(True).getAttempt(),
|
||||
"ipjailfailures": lambda self: self._mi4ip().getAttempt(),
|
||||
# raw ticket info:
|
||||
"raw-ticket": lambda self: repr(self.__ticket)
|
||||
"raw-ticket": lambda self: repr(self.__ticket),
|
||||
# jail info:
|
||||
"jail.banned": lambda self: self.__jail.actions.banManager.size(),
|
||||
"jail.banned_total": lambda self: self.__jail.actions.banManager.getBanTotal(),
|
||||
"jail.found": lambda self: self.__jail.filter.failManager.size(),
|
||||
"jail.found_total": lambda self: self.__jail.filter.failManager.getFailTotal()
|
||||
}
|
||||
|
||||
__slots__ = CallingMap.__slots__ + ('__ticket', '__jail', '__mi4ip')
|
||||
@@ -396,6 +417,11 @@ class Actions(JailThread, Mapping):
|
||||
def copy(self): # pragma: no cover
|
||||
return self.__class__(self.__ticket, self.__jail, self.immutable, self.data.copy())
|
||||
|
||||
def _getBanTime(self):
|
||||
btime = self.__ticket.getBanTime()
|
||||
if btime is None: btime = self.__jail.actions.getBanTime()
|
||||
return int(btime)
|
||||
|
||||
def _mi4ip(self, overalljails=False):
|
||||
"""Gets bans merged once, a helper for lambda(s), prevents stop of executing action by any exception inside.
|
||||
|
||||
@@ -439,7 +465,9 @@ class Actions(JailThread, Mapping):
|
||||
return mi[idx] if mi[idx] is not None else self.__ticket
|
||||
|
||||
|
||||
def __getActionInfo(self, ticket):
|
||||
def _getActionInfo(self, ticket):
|
||||
if not ticket:
|
||||
ticket = BanTicket("", MyTime.time())
|
||||
aInfo = Actions.ActionInfo(ticket, self._jail)
|
||||
return aInfo
|
||||
|
||||
@@ -469,13 +497,19 @@ class Actions(JailThread, Mapping):
|
||||
tickets = self.__getFailTickets(self.banPrecedence)
|
||||
rebanacts = None
|
||||
for ticket in tickets:
|
||||
bTicket = BanManager.createBanTicket(ticket)
|
||||
|
||||
bTicket = BanTicket.wrap(ticket)
|
||||
btime = ticket.getBanTime(self.banManager.getBanTime())
|
||||
ip = bTicket.getIP()
|
||||
aInfo = self.__getActionInfo(bTicket)
|
||||
aInfo = self._getActionInfo(bTicket)
|
||||
reason = {}
|
||||
if self.__banManager.addBanTicket(bTicket, reason=reason):
|
||||
if self.banManager.addBanTicket(bTicket, reason=reason):
|
||||
cnt += 1
|
||||
# report ticket to observer, to check time should be increased and hereafter observer writes ban to database (asynchronous)
|
||||
if Observers.Main is not None and not bTicket.restored:
|
||||
Observers.Main.add('banFound', bTicket, self._jail, btime)
|
||||
logSys.notice("[%s] %sBan %s", self._jail.name, ('' if not bTicket.restored else 'Restore '), ip)
|
||||
# do actions :
|
||||
for name, action in self._actions.iteritems():
|
||||
try:
|
||||
if bTicket.restored and getattr(action, 'norestored', False):
|
||||
@@ -493,7 +527,10 @@ class Actions(JailThread, Mapping):
|
||||
if self.banEpoch: # be sure tickets always have the same ban epoch (default 0):
|
||||
bTicket.banEpoch = self.banEpoch
|
||||
else:
|
||||
bTicket = reason['ticket']
|
||||
if reason.get('expired', 0):
|
||||
logSys.info('[%s] Ignore %s, expired bantime', self._jail.name, ip)
|
||||
continue
|
||||
bTicket = reason.get('ticket', bTicket)
|
||||
# if already banned (otherwise still process some action)
|
||||
if bTicket.banned:
|
||||
# compare time of failure occurrence with time ticket was really banned:
|
||||
@@ -521,15 +558,11 @@ class Actions(JailThread, Mapping):
|
||||
cnt += self.__reBan(bTicket, actions=rebanacts)
|
||||
else: # pragma: no cover - unexpected: ticket is not banned for some reasons - reban using all actions:
|
||||
cnt += self.__reBan(bTicket)
|
||||
# add ban to database:
|
||||
if not bTicket.restored and self._jail.database is not None:
|
||||
# ignore too old (repeated and ignored) tickets,
|
||||
# [todo] replace it with inOperation later (once it gets back-ported):
|
||||
if not reason and bTicket.getTime() >= MyTime.time() - 60:
|
||||
self._jail.database.addBan(self._jail, bTicket)
|
||||
# add ban to database moved to observer (should previously check not already banned
|
||||
# and increase ticket time if "bantime.increment" set)
|
||||
if cnt:
|
||||
logSys.debug("Banned %s / %s, %s ticket(s) in %r", cnt,
|
||||
self.__banManager.getBanTotal(), self.__banManager.size(), self._jail.name)
|
||||
self.banManager.getBanTotal(), self.banManager.size(), self._jail.name)
|
||||
return cnt
|
||||
|
||||
def __reBan(self, ticket, actions=None, log=True):
|
||||
@@ -545,7 +578,7 @@ class Actions(JailThread, Mapping):
|
||||
"""
|
||||
actions = actions or self._actions
|
||||
ip = ticket.getIP()
|
||||
aInfo = self.__getActionInfo(ticket)
|
||||
aInfo = self._getActionInfo(ticket)
|
||||
if log:
|
||||
logSys.notice("[%s] Reban %s%s", self._jail.name, aInfo["ip"], (', action %r' % actions.keys()[0] if len(actions) == 1 else ''))
|
||||
for name, action in actions.iteritems():
|
||||
@@ -566,18 +599,41 @@ class Actions(JailThread, Mapping):
|
||||
ticket.banEpoch = self.banEpoch
|
||||
return 1
|
||||
|
||||
def _prolongBan(self, ticket):
|
||||
# prevent to prolong ticket that was removed in-between,
|
||||
# if it in ban list - ban time already prolonged (and it stays there):
|
||||
if not self.banManager._inBanList(ticket): return
|
||||
# do actions :
|
||||
aInfo = None
|
||||
for name, action in self._actions.iteritems():
|
||||
try:
|
||||
if ticket.restored and getattr(action, 'norestored', False):
|
||||
continue
|
||||
if not action._prolongable:
|
||||
continue
|
||||
if aInfo is None:
|
||||
aInfo = self._getActionInfo(ticket)
|
||||
if not aInfo.immutable: aInfo.reset()
|
||||
action.prolong(aInfo)
|
||||
except Exception as e:
|
||||
logSys.error(
|
||||
"Failed to execute ban jail '%s' action '%s' "
|
||||
"info '%r': %s",
|
||||
self._jail.name, name, aInfo, e,
|
||||
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
|
||||
def __checkUnBan(self, maxCount=None):
|
||||
"""Check for IP address to unban.
|
||||
|
||||
Unban IP addresses which are outdated.
|
||||
"""
|
||||
lst = self.__banManager.unBanList(MyTime.time(), maxCount)
|
||||
lst = self.banManager.unBanList(MyTime.time(), maxCount)
|
||||
for ticket in lst:
|
||||
self.__unBan(ticket)
|
||||
cnt = len(lst)
|
||||
if cnt:
|
||||
logSys.debug("Unbanned %s, %s ticket(s) in %r",
|
||||
cnt, self.__banManager.size(), self._jail.name)
|
||||
cnt, self.banManager.size(), self._jail.name)
|
||||
return cnt
|
||||
|
||||
def __flushBan(self, db=False, actions=None, stop=False):
|
||||
@@ -591,10 +647,10 @@ class Actions(JailThread, Mapping):
|
||||
log = True
|
||||
if actions is None:
|
||||
logSys.debug(" Flush ban list")
|
||||
lst = self.__banManager.flushBanList()
|
||||
lst = self.banManager.flushBanList()
|
||||
else:
|
||||
log = False # don't log "[jail] Unban ..." if removing actions only.
|
||||
lst = iter(self.__banManager)
|
||||
lst = iter(self.banManager)
|
||||
cnt = 0
|
||||
# first we'll execute flush for actions supporting this operation:
|
||||
unbactions = {}
|
||||
@@ -631,7 +687,7 @@ class Actions(JailThread, Mapping):
|
||||
self.__unBan(ticket, actions=actions, log=log)
|
||||
cnt += 1
|
||||
logSys.debug(" Unbanned %s, %s ticket(s) in %r",
|
||||
cnt, self.__banManager.size(), self._jail.name)
|
||||
cnt, self.banManager.size(), self._jail.name)
|
||||
return cnt
|
||||
|
||||
def __unBan(self, ticket, actions=None, log=True):
|
||||
@@ -650,7 +706,7 @@ class Actions(JailThread, Mapping):
|
||||
else:
|
||||
unbactions = actions
|
||||
ip = ticket.getIP()
|
||||
aInfo = self.__getActionInfo(ticket)
|
||||
aInfo = self._getActionInfo(ticket)
|
||||
if log:
|
||||
logSys.notice("[%s] Unban %s", self._jail.name, aInfo["ip"])
|
||||
for name, action in unbactions.iteritems():
|
||||
@@ -674,18 +730,18 @@ class Actions(JailThread, Mapping):
|
||||
logSys.warning("Unsupported extended jail status flavor %r. Supported: %s" % (flavor, supported_flavors))
|
||||
# Always print this information (basic)
|
||||
if flavor != "short":
|
||||
banned = self.__banManager.getBanList()
|
||||
banned = self.banManager.getBanList()
|
||||
cnt = len(banned)
|
||||
else:
|
||||
cnt = self.__banManager.size()
|
||||
cnt = self.banManager.size()
|
||||
ret = [("Currently banned", cnt),
|
||||
("Total banned", self.__banManager.getBanTotal())]
|
||||
("Total banned", self.banManager.getBanTotal())]
|
||||
if flavor != "short":
|
||||
ret += [("Banned IP list", banned)]
|
||||
if flavor == "cymru":
|
||||
cymru_info = self.__banManager.getBanListExtendedCymruInfo()
|
||||
cymru_info = self.banManager.getBanListExtendedCymruInfo()
|
||||
ret += \
|
||||
[("Banned ASN list", self.__banManager.geBanListExtendedASN(cymru_info)),
|
||||
("Banned Country list", self.__banManager.geBanListExtendedCountry(cymru_info)),
|
||||
("Banned RIR list", self.__banManager.geBanListExtendedRIR(cymru_info))]
|
||||
[("Banned ASN list", self.banManager.geBanListExtendedASN(cymru_info)),
|
||||
("Banned Country list", self.banManager.geBanListExtendedCountry(cymru_info)),
|
||||
("Banned RIR list", self.banManager.geBanListExtendedRIR(cymru_info))]
|
||||
return ret
|
||||
|
||||
@@ -66,7 +66,7 @@ class BanManager:
|
||||
# @param value the time
|
||||
|
||||
def setBanTime(self, value):
|
||||
self.__banTime = int(value)
|
||||
self.__banTime = int(value)
|
||||
|
||||
##
|
||||
# Get the ban time.
|
||||
@@ -75,7 +75,7 @@ class BanManager:
|
||||
# @return the time
|
||||
|
||||
def getBanTime(self):
|
||||
return self.__banTime
|
||||
return self.__banTime
|
||||
|
||||
##
|
||||
# Set the total number of banned address.
|
||||
@@ -83,7 +83,7 @@ class BanManager:
|
||||
# @param value total number
|
||||
|
||||
def setBanTotal(self, value):
|
||||
self.__banTotal = value
|
||||
self.__banTotal = value
|
||||
|
||||
##
|
||||
# Get the total number of banned address.
|
||||
@@ -91,15 +91,29 @@ class BanManager:
|
||||
# @return the total number
|
||||
|
||||
def getBanTotal(self):
|
||||
return self.__banTotal
|
||||
return self.__banTotal
|
||||
|
||||
##
|
||||
# Returns a copy of the IP list.
|
||||
#
|
||||
# @return IP list
|
||||
|
||||
def getBanList(self):
|
||||
return list(self.__banList.keys())
|
||||
def getBanList(self, ordered=False, withTime=False):
|
||||
if not ordered:
|
||||
return list(self.__banList.keys())
|
||||
with self.__lock:
|
||||
lst = []
|
||||
for ticket in self.__banList.itervalues():
|
||||
eob = ticket.getEndOfBanTime(self.__banTime)
|
||||
lst.append((ticket,eob))
|
||||
lst.sort(key=lambda t: t[1])
|
||||
t2s = MyTime.time2str
|
||||
if withTime:
|
||||
return ['%s \t%s + %d = %s' % (
|
||||
t[0].getID(),
|
||||
t2s(t[0].getTime()), t[0].getBanTime(self.__banTime), t2s(t[1])
|
||||
) for t in lst]
|
||||
return [t[0].getID() for t in lst]
|
||||
|
||||
##
|
||||
# Returns a iterator to ban list (used in reload, so idle).
|
||||
@@ -108,7 +122,7 @@ class BanManager:
|
||||
|
||||
def __iter__(self):
|
||||
# ensure iterator is safe - traverse over the list in snapshot created within lock (GIL):
|
||||
return iter(list(self.__banList.values()))
|
||||
return iter(list(self.__banList.values()))
|
||||
|
||||
##
|
||||
# Returns normalized value
|
||||
@@ -244,21 +258,6 @@ class BanManager:
|
||||
logSys.exception(e)
|
||||
return []
|
||||
|
||||
##
|
||||
# Create a ban ticket.
|
||||
#
|
||||
# Create a BanTicket from a FailTicket. The timestamp of the BanTicket
|
||||
# is the current time. This is a static method.
|
||||
# @param ticket the FailTicket
|
||||
# @return a BanTicket
|
||||
|
||||
@staticmethod
|
||||
def createBanTicket(ticket):
|
||||
# we should always use correct time to calculate correct end time (ban time is variable now,
|
||||
# + possible double banning by restore from database and from log file)
|
||||
# so use as lastTime always time from ticket.
|
||||
return BanTicket(ticket=ticket)
|
||||
|
||||
##
|
||||
# Add a ban ticket.
|
||||
#
|
||||
@@ -268,6 +267,9 @@ class BanManager:
|
||||
|
||||
def addBanTicket(self, ticket, reason={}):
|
||||
eob = ticket.getEndOfBanTime(self.__banTime)
|
||||
if eob < MyTime.time():
|
||||
reason['expired'] = 1
|
||||
return False
|
||||
with self.__lock:
|
||||
# check already banned
|
||||
fid = ticket.getID()
|
||||
@@ -289,6 +291,7 @@ class BanManager:
|
||||
# not yet banned - add new one:
|
||||
self.__banList[fid] = ticket
|
||||
self.__banTotal += 1
|
||||
ticket.incrBanCount()
|
||||
# correct next unban time:
|
||||
if self._nextUnbanTime > eob:
|
||||
self._nextUnbanTime = eob
|
||||
|
||||
+167
-44
@@ -138,7 +138,7 @@ class Fail2BanDb(object):
|
||||
filename
|
||||
purgeage
|
||||
"""
|
||||
__version__ = 2
|
||||
__version__ = 4
|
||||
# Note all SCRIPTS strings must end in ';' for py26 compatibility
|
||||
_CREATE_SCRIPTS = (
|
||||
('fail2banDb', "CREATE TABLE IF NOT EXISTS fail2banDb(version INTEGER);")
|
||||
@@ -166,21 +166,36 @@ class Fail2BanDb(object):
|
||||
"jail TEXT NOT NULL, " \
|
||||
"ip TEXT, " \
|
||||
"timeofban INTEGER NOT NULL, " \
|
||||
"bantime INTEGER NOT NULL, " \
|
||||
"bancount INTEGER NOT NULL default 1, " \
|
||||
"data JSON, " \
|
||||
"FOREIGN KEY(jail) REFERENCES jails(name) " \
|
||||
");" \
|
||||
"CREATE INDEX IF NOT EXISTS bans_jail_timeofban_ip ON bans(jail, timeofban);" \
|
||||
"CREATE INDEX IF NOT EXISTS bans_jail_ip ON bans(jail, ip);" \
|
||||
"CREATE INDEX IF NOT EXISTS bans_ip ON bans(ip);")
|
||||
,('bips', "CREATE TABLE IF NOT EXISTS bips(" \
|
||||
"ip TEXT NOT NULL, " \
|
||||
"jail TEXT NOT NULL, " \
|
||||
"timeofban INTEGER NOT NULL, " \
|
||||
"bantime INTEGER NOT NULL, " \
|
||||
"bancount INTEGER NOT NULL default 1, " \
|
||||
"data JSON, " \
|
||||
"PRIMARY KEY(ip, jail), " \
|
||||
"FOREIGN KEY(jail) REFERENCES jails(name) " \
|
||||
");" \
|
||||
"CREATE INDEX IF NOT EXISTS bips_timeofban ON bips(timeofban);" \
|
||||
"CREATE INDEX IF NOT EXISTS bips_ip ON bips(ip);")
|
||||
)
|
||||
_CREATE_TABS = dict(_CREATE_SCRIPTS)
|
||||
|
||||
|
||||
def __init__(self, filename, purgeAge=24*60*60):
|
||||
def __init__(self, filename, purgeAge=24*60*60, outDatedFactor=3):
|
||||
self.maxMatches = 10
|
||||
self._lock = RLock()
|
||||
self._dbFilename = filename
|
||||
self._purgeAge = purgeAge
|
||||
self._outDatedFactor = outDatedFactor;
|
||||
self._connectDB()
|
||||
|
||||
def _connectDB(self, checkIntegrity=False):
|
||||
@@ -366,13 +381,32 @@ class Fail2BanDb(object):
|
||||
|
||||
if version < 2 and self._tableExists(cur, "logs"):
|
||||
cur.executescript("BEGIN TRANSACTION;"
|
||||
"CREATE TEMPORARY TABLE logs_temp AS SELECT * FROM logs;"
|
||||
"DROP TABLE logs;"
|
||||
"%s;"
|
||||
"INSERT INTO logs SELECT * from logs_temp;"
|
||||
"DROP TABLE logs_temp;"
|
||||
"UPDATE fail2banDb SET version = 2;"
|
||||
"COMMIT;" % Fail2BanDb._CREATE_TABS['logs'])
|
||||
"CREATE TEMPORARY TABLE logs_temp AS SELECT * FROM logs;"
|
||||
"DROP TABLE logs;"
|
||||
"%s;"
|
||||
"INSERT INTO logs SELECT * from logs_temp;"
|
||||
"DROP TABLE logs_temp;"
|
||||
"UPDATE fail2banDb SET version = 2;"
|
||||
"COMMIT;" % Fail2BanDb._CREATE_TABS['logs'])
|
||||
|
||||
if version < 3 and self._tableExists(cur, "bans"):
|
||||
# set ban-time to -2 (note it means rather unknown, as persistent, will be fixed by restore):
|
||||
cur.executescript("BEGIN TRANSACTION;"
|
||||
"CREATE TEMPORARY TABLE bans_temp AS SELECT jail, ip, timeofban, -2 as bantime, 1 as bancount, data FROM bans;"
|
||||
"DROP TABLE bans;"
|
||||
"%s;\n"
|
||||
"INSERT INTO bans SELECT * from bans_temp;"
|
||||
"DROP TABLE bans_temp;"
|
||||
"COMMIT;" % Fail2BanDb._CREATE_TABS['bans'])
|
||||
if version < 4 and not self._tableExists(cur, "bips"):
|
||||
cur.executescript("BEGIN TRANSACTION;"
|
||||
"%s;\n"
|
||||
"UPDATE fail2banDb SET version = 4;"
|
||||
"COMMIT;" % Fail2BanDb._CREATE_TABS['bips'])
|
||||
if self._tableExists(cur, "bans"):
|
||||
cur.execute(
|
||||
"INSERT OR REPLACE INTO bips(ip, jail, timeofban, bantime, bancount, data)"
|
||||
" SELECT ip, jail, timeofban, bantime, bancount, data FROM bans order by timeofban")
|
||||
|
||||
cur.execute("SELECT version FROM fail2banDb LIMIT 1")
|
||||
return cur.fetchone()[0]
|
||||
@@ -468,7 +502,7 @@ class Fail2BanDb(object):
|
||||
except TypeError:
|
||||
firstLineMD5 = None
|
||||
|
||||
if not firstLineMD5 and (pos or md5):
|
||||
if firstLineMD5 is None and (pos or md5 is not None):
|
||||
cur.execute(
|
||||
"INSERT OR REPLACE INTO logs(jail, path, firstlinemd5, lastfilepos) "
|
||||
"VALUES(?, ?, ?, ?)", (jail.name, name, md5, pos))
|
||||
@@ -585,8 +619,13 @@ class Fail2BanDb(object):
|
||||
data = data.copy()
|
||||
del data['matches']
|
||||
cur.execute(
|
||||
"INSERT INTO bans(jail, ip, timeofban, data) VALUES(?, ?, ?, ?)",
|
||||
(jail.name, ip, int(round(ticket.getTime())), data))
|
||||
"INSERT INTO bans(jail, ip, timeofban, bantime, bancount, data) VALUES(?, ?, ?, ?, ?, ?)",
|
||||
(jail.name, ip, int(round(ticket.getTime())), ticket.getBanTime(jail.actions.getBanTime()), ticket.getBanCount(),
|
||||
data))
|
||||
cur.execute(
|
||||
"INSERT OR REPLACE INTO bips(ip, jail, timeofban, bantime, bancount, data) VALUES(?, ?, ?, ?, ?, ?)",
|
||||
(ip, jail.name, int(round(ticket.getTime())), ticket.getBanTime(jail.actions.getBanTime()), ticket.getBanCount(),
|
||||
data))
|
||||
|
||||
@commitandrollback
|
||||
def delBan(self, cur, jail, *args):
|
||||
@@ -599,16 +638,20 @@ class Fail2BanDb(object):
|
||||
args : list of IP
|
||||
IPs to be removed, if not given all tickets of jail will be removed.
|
||||
"""
|
||||
query = "DELETE FROM bans WHERE jail = ?"
|
||||
query1 = "DELETE FROM bips WHERE jail = ?"
|
||||
query2 = "DELETE FROM bans WHERE jail = ?"
|
||||
queryArgs = [jail.name];
|
||||
if not len(args):
|
||||
cur.execute(query, queryArgs);
|
||||
cur.execute(query1, queryArgs);
|
||||
cur.execute(query2, queryArgs);
|
||||
return
|
||||
query += " AND ip = ?"
|
||||
query1 += " AND ip = ?"
|
||||
query2 += " AND ip = ?"
|
||||
queryArgs.append('');
|
||||
for ip in args:
|
||||
queryArgs[1] = str(ip);
|
||||
cur.execute(query, queryArgs);
|
||||
cur.execute(query1, queryArgs);
|
||||
cur.execute(query2, queryArgs);
|
||||
|
||||
@commitandrollback
|
||||
def _getBans(self, cur, jail=None, bantime=None, ip=None):
|
||||
@@ -726,18 +769,42 @@ class Fail2BanDb(object):
|
||||
self._bansMergedCache[cacheKey] = tickets if ip is None else ticket
|
||||
return tickets if ip is None else ticket
|
||||
|
||||
@commitandrollback
|
||||
def getBan(self, cur, ip, jail=None, forbantime=None, overalljails=None, fromtime=None):
|
||||
ip = str(ip)
|
||||
if not overalljails:
|
||||
query = "SELECT bancount, timeofban, bantime FROM bips"
|
||||
else:
|
||||
query = "SELECT sum(bancount), max(timeofban), sum(bantime) FROM bips"
|
||||
query += " WHERE ip = ?"
|
||||
queryArgs = [ip]
|
||||
if not overalljails and jail is not None:
|
||||
query += " AND jail=?"
|
||||
queryArgs.append(jail.name)
|
||||
if forbantime is not None:
|
||||
query += " AND timeofban > ?"
|
||||
queryArgs.append(MyTime.time() - forbantime)
|
||||
if fromtime is not None:
|
||||
query += " AND timeofban > ?"
|
||||
queryArgs.append(fromtime)
|
||||
if overalljails or jail is None:
|
||||
query += " GROUP BY ip ORDER BY timeofban DESC LIMIT 1"
|
||||
cur = self._db.cursor()
|
||||
# repack iterator as long as in lock:
|
||||
return list(cur.execute(query, queryArgs))
|
||||
|
||||
def _getCurrentBans(self, cur, jail = None, ip = None, forbantime=None, fromtime=None):
|
||||
if fromtime is None:
|
||||
fromtime = MyTime.time()
|
||||
queryArgs = []
|
||||
if jail is not None:
|
||||
query = "SELECT ip, timeofban, data FROM bans WHERE jail=?"
|
||||
query = "SELECT ip, timeofban, bantime, bancount, data FROM bips WHERE jail=?"
|
||||
queryArgs.append(jail.name)
|
||||
else:
|
||||
query = "SELECT ip, max(timeofban), data FROM bans WHERE 1"
|
||||
query = "SELECT ip, max(timeofban), bantime, bancount, data FROM bips WHERE 1"
|
||||
if ip is not None:
|
||||
query += " AND ip=?"
|
||||
queryArgs.append(ip)
|
||||
query += " AND (timeofban + bantime > ? OR bantime <= -1)"
|
||||
queryArgs.append(fromtime)
|
||||
if forbantime not in (None, -1): # not specified or persistent (all)
|
||||
query += " AND timeofban > ?"
|
||||
queryArgs.append(fromtime - forbantime)
|
||||
@@ -748,33 +815,90 @@ class Fail2BanDb(object):
|
||||
cur = self._db.cursor()
|
||||
return cur.execute(query, queryArgs)
|
||||
|
||||
def getCurrentBans(self, jail = None, ip = None, forbantime=None, fromtime=None, maxmatches=None):
|
||||
@commitandrollback
|
||||
def getCurrentBans(self, cur, jail=None, ip=None, forbantime=None, fromtime=None,
|
||||
correctBanTime=True, maxmatches=None
|
||||
):
|
||||
"""Reads tickets (with merged info) currently affected from ban from the database.
|
||||
|
||||
There are all the tickets corresponding parameters jail/ip, forbantime,
|
||||
fromtime (normally now).
|
||||
|
||||
If correctBanTime specified (default True) it will fix the restored ban-time
|
||||
(and therefore endOfBan) of the ticket (normally it is ban-time of jail as maximum)
|
||||
for all tickets with ban-time greater (or persistent).
|
||||
"""
|
||||
if fromtime is None:
|
||||
fromtime = MyTime.time()
|
||||
tickets = []
|
||||
ticket = None
|
||||
if correctBanTime is True:
|
||||
correctBanTime = jail.getMaxBanTime() if jail is not None else None
|
||||
# don't change if persistent allowed:
|
||||
if correctBanTime == -1: correctBanTime = None
|
||||
|
||||
with self._lock:
|
||||
results = list(self._getCurrentBans(self._db.cursor(),
|
||||
jail=jail, ip=ip, forbantime=forbantime, fromtime=fromtime))
|
||||
|
||||
if results:
|
||||
for banip, timeofban, data in results:
|
||||
# logSys.debug('restore ticket %r, %r, %r', banip, timeofban, data)
|
||||
ticket = FailTicket(banip, timeofban, data=data)
|
||||
# filter matches if expected (current count > as maxmatches specified):
|
||||
if maxmatches is None:
|
||||
maxmatches = self.maxMatches
|
||||
if maxmatches:
|
||||
matches = ticket.getMatches()
|
||||
if matches and len(matches) > maxmatches:
|
||||
ticket.setMatches(matches[-maxmatches:])
|
||||
else:
|
||||
ticket.setMatches(None)
|
||||
# logSys.debug('restored ticket: %r', ticket)
|
||||
if ip is not None: return ticket
|
||||
tickets.append(ticket)
|
||||
for ticket in self._getCurrentBans(cur, jail=jail, ip=ip,
|
||||
forbantime=forbantime, fromtime=fromtime
|
||||
):
|
||||
# can produce unpack error (database may return sporadical wrong-empty row):
|
||||
try:
|
||||
banip, timeofban, bantime, bancount, data = ticket
|
||||
# additionally check for empty values:
|
||||
if banip is None or banip == "": # pragma: no cover
|
||||
raise ValueError('unexpected value %r' % (banip,))
|
||||
# if bantime unknown (after upgrade-db from earlier version), just use min known ban-time:
|
||||
if bantime == -2: # todo: remove it in future version
|
||||
bantime = jail.actions.getBanTime() if jail is not None else (
|
||||
correctBanTime if correctBanTime else 600)
|
||||
elif correctBanTime and correctBanTime >= 0:
|
||||
# if persistent ban (or greater as max), use current max-bantime of the jail:
|
||||
if bantime == -1 or bantime > correctBanTime:
|
||||
bantime = correctBanTime
|
||||
# after correction check the end of ban again:
|
||||
if bantime != -1 and timeofban + bantime <= fromtime:
|
||||
# not persistent and too old - ignore it:
|
||||
logSys.debug("ignore ticket (with new max ban-time %r): too old %r <= %r, ticket: %r",
|
||||
bantime, timeofban + bantime, fromtime, ticket)
|
||||
continue
|
||||
except ValueError as e: # pragma: no cover
|
||||
logSys.debug("get current bans: ignore row %r - %s", ticket, e)
|
||||
continue
|
||||
# logSys.debug('restore ticket %r, %r, %r', banip, timeofban, data)
|
||||
ticket = FailTicket(banip, timeofban, data=data)
|
||||
# filter matches if expected (current count > as maxmatches specified):
|
||||
if maxmatches is None:
|
||||
maxmatches = self.maxMatches
|
||||
if maxmatches:
|
||||
matches = ticket.getMatches()
|
||||
if matches and len(matches) > maxmatches:
|
||||
ticket.setMatches(matches[-maxmatches:])
|
||||
else:
|
||||
ticket.setMatches(None)
|
||||
# logSys.debug('restored ticket: %r', ticket)
|
||||
ticket.setBanTime(bantime)
|
||||
ticket.setBanCount(bancount)
|
||||
if ip is not None: return ticket
|
||||
tickets.append(ticket)
|
||||
|
||||
return tickets
|
||||
|
||||
def _cleanjails(self, cur):
|
||||
"""Remove empty jails jails and log files from database.
|
||||
"""
|
||||
cur.execute(
|
||||
"DELETE FROM jails WHERE enabled = 0 "
|
||||
"AND NOT EXISTS(SELECT * FROM bans WHERE jail = jails.name) "
|
||||
"AND NOT EXISTS(SELECT * FROM bips WHERE jail = jails.name)")
|
||||
|
||||
def _purge_bips(self, cur):
|
||||
"""Purge old bad ips (jails and log files from database).
|
||||
Currently it is timed out IP, whose time since last ban is several times out-dated (outDatedFactor is default 3).
|
||||
Permanent banned ips will be never removed.
|
||||
"""
|
||||
cur.execute(
|
||||
"DELETE FROM bips WHERE timeofban < ? and bantime != -1 and (timeofban + (bantime * ?)) < ?",
|
||||
(int(MyTime.time()) - self._purgeAge, self._outDatedFactor, int(MyTime.time()) - self._purgeAge))
|
||||
|
||||
@commitandrollback
|
||||
def purge(self, cur):
|
||||
"""Purge old bans, jails and log files from database.
|
||||
@@ -783,7 +907,6 @@ class Fail2BanDb(object):
|
||||
cur.execute(
|
||||
"DELETE FROM bans WHERE timeofban < ?",
|
||||
(MyTime.time() - self._purgeAge, ))
|
||||
cur.execute(
|
||||
"DELETE FROM jails WHERE enabled = 0 "
|
||||
"AND NOT EXISTS(SELECT * FROM bans WHERE jail = jails.name)")
|
||||
self._purge_bips(cur)
|
||||
self._cleanjails(cur)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ from ..helpers import getLogger
|
||||
# Gets the instance of the logger.
|
||||
logSys = getLogger(__name__)
|
||||
|
||||
logLevel = 6
|
||||
logLevel = 5
|
||||
|
||||
RE_DATE_PREMATCH = re.compile(r"(?<!\\)\{DATE\}", re.IGNORECASE)
|
||||
DD_patternCache = Utils.Cache(maxCount=1000, maxTime=60*60)
|
||||
@@ -365,10 +365,10 @@ class DateDetector(object):
|
||||
# with space or some special char), otherwise possible collision/pattern switch:
|
||||
if ((
|
||||
line[distance-1:distance] == self.__lastPos[1] or
|
||||
(line[distance] == self.__lastPos[2] and not self.__lastPos[2].isalnum())
|
||||
(line[distance:distance+1] == self.__lastPos[2] and not self.__lastPos[2].isalnum())
|
||||
) and (
|
||||
line[endpos:endpos+1] == self.__lastEndPos[2] or
|
||||
(line[endpos-1] == self.__lastEndPos[1] and not self.__lastEndPos[1].isalnum())
|
||||
(line[endpos-1:endpos] == self.__lastEndPos[1] and not self.__lastEndPos[1].isalnum())
|
||||
)):
|
||||
# search in line part only:
|
||||
log(logLevel-1, " boundaries are correct, search in part %r", line[distance:endpos])
|
||||
|
||||
@@ -136,7 +136,7 @@ class DateTemplate(object):
|
||||
# remove possible special pattern "**" in front and end of regex:
|
||||
regex = RE_DEL_WRD_BOUNDS[0].sub(RE_DEL_WRD_BOUNDS[1], regex)
|
||||
self._regex = regex
|
||||
logSys.log(7, ' constructed regex %s', regex)
|
||||
logSys.log(4, ' constructed regex %s', regex)
|
||||
self._cRegex = None
|
||||
|
||||
regex = property(getRegex, setRegex, doc=
|
||||
@@ -159,6 +159,7 @@ class DateTemplate(object):
|
||||
"""
|
||||
if not self._cRegex:
|
||||
self._compileRegex()
|
||||
logSys.log(4, " search %s", self.regex)
|
||||
dateMatch = self._cRegex.search(line, *args); # pos, endpos
|
||||
if dateMatch:
|
||||
self.hits += 1
|
||||
|
||||
@@ -27,7 +27,7 @@ __license__ = "GPL"
|
||||
from threading import Lock
|
||||
import logging
|
||||
|
||||
from .ticket import FailTicket
|
||||
from .ticket import FailTicket, BanTicket
|
||||
from ..helpers import getLogger, BgService
|
||||
|
||||
# Gets the instance of the logger.
|
||||
@@ -69,7 +69,7 @@ class FailManager:
|
||||
def getMaxTime(self):
|
||||
return self.__maxTime
|
||||
|
||||
def addFailure(self, ticket, count=1):
|
||||
def addFailure(self, ticket, count=1, observed=False):
|
||||
attempts = 1
|
||||
with self.__lock:
|
||||
fid = ticket.getID()
|
||||
@@ -96,11 +96,14 @@ class FailManager:
|
||||
else:
|
||||
fData.setMatches(None)
|
||||
except KeyError:
|
||||
# not found - already banned - prevent to add failure if comes from observer:
|
||||
if observed or isinstance(ticket, BanTicket):
|
||||
return ticket.getRetry()
|
||||
# if already FailTicket - add it direct, otherwise create (using copy all ticket data):
|
||||
if isinstance(ticket, FailTicket):
|
||||
fData = ticket;
|
||||
else:
|
||||
fData = FailTicket(ticket=ticket)
|
||||
fData = FailTicket.wrap(ticket)
|
||||
if count > ticket.getAttempt():
|
||||
fData.setRetry(count)
|
||||
self.__failList[fid] = fData
|
||||
@@ -124,9 +127,10 @@ class FailManager:
|
||||
return len(self.__failList)
|
||||
|
||||
def cleanup(self, time):
|
||||
time -= self.__maxTime
|
||||
with self.__lock:
|
||||
todelete = [fid for fid,item in self.__failList.iteritems() \
|
||||
if item.getTime() + self.__maxTime <= time]
|
||||
if item.getTime() <= time]
|
||||
if len(todelete) == len(self.__failList):
|
||||
# remove all:
|
||||
self.__failList = dict()
|
||||
@@ -140,7 +144,7 @@ class FailManager:
|
||||
else:
|
||||
# create new dictionary without items to be deleted:
|
||||
self.__failList = dict((fid,item) for fid,item in self.__failList.iteritems() \
|
||||
if item.getTime() + self.__maxTime > time)
|
||||
if item.getTime() > time)
|
||||
self.__bgSvc.service()
|
||||
|
||||
def delFailure(self, fid):
|
||||
|
||||
@@ -87,20 +87,24 @@ RH4TAG = {
|
||||
|
||||
# default failure groups map for customizable expressions (with different group-id):
|
||||
R_MAP = {
|
||||
"ID": "fid",
|
||||
"PORT": "fport",
|
||||
"id": "fid",
|
||||
"port": "fport",
|
||||
}
|
||||
|
||||
def mapTag2Opt(tag):
|
||||
try: # if should be mapped:
|
||||
return R_MAP[tag]
|
||||
except KeyError:
|
||||
return tag.lower()
|
||||
tag = tag.lower()
|
||||
return R_MAP.get(tag, tag)
|
||||
|
||||
|
||||
# alternate names to be merged, e. g. alt_user_1 -> user ...
|
||||
# complex names:
|
||||
# ALT_ - alternate names to be merged, e. g. alt_user_1 -> user ...
|
||||
ALTNAME_PRE = 'alt_'
|
||||
ALTNAME_CRE = re.compile(r'^' + ALTNAME_PRE + r'(.*)(?:_\d+)?$')
|
||||
# TUPLE_ - names of parts to be combined to single value as tuple
|
||||
TUPNAME_PRE = 'tuple_'
|
||||
|
||||
COMPLNAME_PRE = (ALTNAME_PRE, TUPNAME_PRE)
|
||||
COMPLNAME_CRE = re.compile(r'^(' + '|'.join(COMPLNAME_PRE) + r')(.*?)(?:_\d+)?$')
|
||||
|
||||
|
||||
##
|
||||
# Regular expression class.
|
||||
@@ -127,19 +131,27 @@ class Regex:
|
||||
try:
|
||||
self._regexObj = re.compile(regex, re.MULTILINE if multiline else 0)
|
||||
self._regex = regex
|
||||
self._altValues = {}
|
||||
self._altValues = []
|
||||
self._tupleValues = []
|
||||
for k in filter(
|
||||
lambda k: len(k) > len(ALTNAME_PRE) and k.startswith(ALTNAME_PRE),
|
||||
self._regexObj.groupindex
|
||||
lambda k: len(k) > len(COMPLNAME_PRE[0]), self._regexObj.groupindex
|
||||
):
|
||||
n = ALTNAME_CRE.match(k).group(1)
|
||||
self._altValues[k] = n
|
||||
self._altValues = list(self._altValues.items()) if len(self._altValues) else None
|
||||
n = COMPLNAME_CRE.match(k)
|
||||
if n:
|
||||
g, n = n.group(1), mapTag2Opt(n.group(2))
|
||||
if g == ALTNAME_PRE:
|
||||
self._altValues.append((k,n))
|
||||
else:
|
||||
self._tupleValues.append((k,n))
|
||||
self._altValues.sort()
|
||||
self._tupleValues.sort()
|
||||
self._altValues = self._altValues if len(self._altValues) else None
|
||||
self._tupleValues = self._tupleValues if len(self._tupleValues) else None
|
||||
except sre_constants.error:
|
||||
raise RegexException("Unable to compile regular expression '%s'" %
|
||||
regex)
|
||||
# set fetch handler depending on presence of alternate tags:
|
||||
self.getGroups = self._getGroupsWithAlt if self._altValues else self._getGroups
|
||||
# set fetch handler depending on presence of alternate (or tuple) tags:
|
||||
self.getGroups = self._getGroupsWithAlt if (self._altValues or self._tupleValues) else self._getGroups
|
||||
|
||||
def __str__(self):
|
||||
return "%s(%r)" % (self.__class__.__name__, self._regex)
|
||||
@@ -284,12 +296,23 @@ class Regex:
|
||||
|
||||
def _getGroupsWithAlt(self):
|
||||
fail = self._matchCache.groupdict()
|
||||
# merge alternate values (e. g. 'alt_user_1' -> 'user' or 'alt_host' -> 'host'):
|
||||
#fail = fail.copy()
|
||||
for k,n in self._altValues:
|
||||
v = fail.get(k)
|
||||
if v and not fail.get(n):
|
||||
fail[n] = v
|
||||
# merge alternate values (e. g. 'alt_user_1' -> 'user' or 'alt_host' -> 'host'):
|
||||
if self._altValues:
|
||||
for k,n in self._altValues:
|
||||
v = fail.get(k)
|
||||
if v and not fail.get(n):
|
||||
fail[n] = v
|
||||
# combine tuple values (e. g. 'id', 'tuple_id' ... 'tuple_id_N' -> 'id'):
|
||||
if self._tupleValues:
|
||||
for k,n in self._tupleValues:
|
||||
v = fail.get(k)
|
||||
t = fail.get(n)
|
||||
if isinstance(t, tuple):
|
||||
t += (v,)
|
||||
else:
|
||||
t = (t,v,)
|
||||
fail[n] = t
|
||||
return fail
|
||||
|
||||
def getGroups(self): # pragma: no cover - abstract function (replaced in __init__)
|
||||
|
||||
+243
-86
@@ -33,6 +33,7 @@ import time
|
||||
from .actions import Actions
|
||||
from .failmanager import FailManagerEmpty, FailManager
|
||||
from .ipdns import DNSUtils, IPAddr
|
||||
from .observer import Observers
|
||||
from .ticket import FailTicket
|
||||
from .jailthread import JailThread
|
||||
from .datedetector import DateDetector, validateTimeZone
|
||||
@@ -93,6 +94,8 @@ class Filter(JailThread):
|
||||
## Store last time stamp, applicable for multi-line
|
||||
self.__lastTimeText = ""
|
||||
self.__lastDate = None
|
||||
## Next service (cleanup) time
|
||||
self.__nextSvcTime = -(1<<63)
|
||||
## if set, treat log lines without explicit time zone to be in this time zone
|
||||
self.__logtimezone = None
|
||||
## Default or preferred encoding (to decode bytes from file or journal):
|
||||
@@ -102,6 +105,10 @@ class Filter(JailThread):
|
||||
## Error counter (protected, so can be used in filter implementations)
|
||||
## if it reached 100 (at once), run-cycle will go idle
|
||||
self._errors = 0
|
||||
## Next time to update log or journal position in database:
|
||||
self._nextUpdateTM = 0
|
||||
## Pending updates (must be executed at next update time or during stop):
|
||||
self._pendDBUpdates = {}
|
||||
## return raw host (host is not dns):
|
||||
self.returnRawHost = False
|
||||
## check each regex (used for test purposes):
|
||||
@@ -114,10 +121,10 @@ class Filter(JailThread):
|
||||
self.checkFindTime = True
|
||||
## shows that filter is in operation mode (processing new messages):
|
||||
self.inOperation = True
|
||||
## if true prevents against retarded banning in case of RC by too many failures (disabled only for test purposes):
|
||||
self.banASAP = True
|
||||
## Ticks counter
|
||||
self.ticks = 0
|
||||
## Processed lines counter
|
||||
self.procLines = 0
|
||||
## Thread name:
|
||||
self.name="f2b/f."+self.jailName
|
||||
|
||||
@@ -441,12 +448,23 @@ class Filter(JailThread):
|
||||
|
||||
def performBan(self, ip=None):
|
||||
"""Performs a ban for IPs (or given ip) that are reached maxretry of the jail."""
|
||||
try: # pragma: no branch - exception is the only way out
|
||||
while True:
|
||||
while True:
|
||||
try:
|
||||
ticket = self.failManager.toBan(ip)
|
||||
self.jail.putFailTicket(ticket)
|
||||
except FailManagerEmpty:
|
||||
self.failManager.cleanup(MyTime.time())
|
||||
except FailManagerEmpty:
|
||||
break
|
||||
self.jail.putFailTicket(ticket)
|
||||
if ip: break
|
||||
self.performSvc()
|
||||
|
||||
def performSvc(self, force=False):
|
||||
"""Performs a service tasks (clean failure list)."""
|
||||
tm = MyTime.time()
|
||||
# avoid too early clean up:
|
||||
if force or tm >= self.__nextSvcTime:
|
||||
self.__nextSvcTime = tm + 5
|
||||
# clean up failure list:
|
||||
self.failManager.cleanup(tm)
|
||||
|
||||
def addAttempt(self, ip, *matches):
|
||||
"""Generate a failed attempt for ip"""
|
||||
@@ -605,6 +623,7 @@ class Filter(JailThread):
|
||||
noDate = False
|
||||
if date:
|
||||
tupleLine = line
|
||||
line = "".join(line)
|
||||
self.__lastTimeText = tupleLine[1]
|
||||
self.__lastDate = date
|
||||
else:
|
||||
@@ -641,30 +660,37 @@ class Filter(JailThread):
|
||||
if self.__lastDate and self.__lastDate > MyTime.time() - 60:
|
||||
tupleLine = ("", self.__lastTimeText, line)
|
||||
date = self.__lastDate
|
||||
elif self.checkFindTime and self.inOperation:
|
||||
date = MyTime.time()
|
||||
|
||||
if self.checkFindTime:
|
||||
if self.checkFindTime and date is not None:
|
||||
# if in operation (modifications have been really found):
|
||||
if self.inOperation:
|
||||
# if weird date - we'd simulate now for timeing issue (too large deviation from now):
|
||||
if (date is None or date < MyTime.time() - 60 or date > MyTime.time() + 60):
|
||||
# log time zone issue as warning once per day:
|
||||
delta = int(date - MyTime.time())
|
||||
if abs(delta) > 60:
|
||||
# log timing issue as warning once per day:
|
||||
self._logWarnOnce("_next_simByTimeWarn",
|
||||
("Simulate NOW in operation since found time has too large deviation %s ~ %s +/- %s",
|
||||
date, MyTime.time(), 60),
|
||||
("Please check jail has possibly a timezone issue. Line with odd timestamp: %s",
|
||||
line))
|
||||
("Detected a log entry %s %s the current time in operation mode. "
|
||||
"This looks like a %s problem. Treating such entries as if they just happened.",
|
||||
MyTime.seconds2str(abs(delta)), "before" if delta < 0 else "after",
|
||||
"latency" if -3300 <= delta < 0 else "timezone"
|
||||
),
|
||||
("Please check a jail for a timing issue. Line with odd timestamp: %s",
|
||||
line))
|
||||
# simulate now as date:
|
||||
date = MyTime.time()
|
||||
self.__lastDate = date
|
||||
else:
|
||||
# in initialization (restore) phase, if too old - ignore:
|
||||
if date is not None and date < MyTime.time() - self.getFindTime():
|
||||
if date < MyTime.time() - self.getFindTime():
|
||||
# log time zone issue as warning once per day:
|
||||
self._logWarnOnce("_next_ignByTimeWarn",
|
||||
("Ignore line since time %s < %s - %s",
|
||||
date, MyTime.time(), self.getFindTime()),
|
||||
("Please check jail has possibly a timezone issue. Line with odd timestamp: %s",
|
||||
line))
|
||||
("Ignoring all log entries older than %ss; these are probably" +
|
||||
" messages generated while fail2ban was not running.",
|
||||
self.getFindTime()),
|
||||
("Please check a jail for a timing issue. Line with odd timestamp: %s",
|
||||
line))
|
||||
# ignore - too old (obsolete) entry:
|
||||
return []
|
||||
|
||||
@@ -689,13 +715,20 @@ class Filter(JailThread):
|
||||
if self._inIgnoreIPList(ip, tick):
|
||||
continue
|
||||
logSys.info(
|
||||
"[%s] Found %s - %s", self.jailName, ip, datetime.datetime.fromtimestamp(unixTime).strftime("%Y-%m-%d %H:%M:%S")
|
||||
"[%s] Found %s - %s", self.jailName, ip, MyTime.time2str(unixTime)
|
||||
)
|
||||
attempts = self.failManager.addFailure(tick)
|
||||
# avoid RC on busy filter (too many failures) - if attempts for IP/ID reached maxretry,
|
||||
# we can speedup ban, so do it as soon as possible:
|
||||
if self.banASAP and attempts >= self.failManager.getMaxRetry():
|
||||
if attempts >= self.failManager.getMaxRetry():
|
||||
self.performBan(ip)
|
||||
# report to observer - failure was found, for possibly increasing of it retry counter (asynchronous)
|
||||
if Observers.Main is not None:
|
||||
Observers.Main.add('failureFound', self.failManager, self.jail, tick)
|
||||
self.procLines += 1
|
||||
# every 100 lines check need to perform service tasks:
|
||||
if self.procLines % 100 == 0:
|
||||
self.performSvc()
|
||||
# reset (halve) error counter (successfully processed line):
|
||||
if self._errors:
|
||||
self._errors //= 2
|
||||
@@ -705,7 +738,7 @@ class Filter(JailThread):
|
||||
# incr common error counter:
|
||||
self.commonError()
|
||||
|
||||
def commonError(self):
|
||||
def commonError(self, reason="common", exc=None):
|
||||
# incr error counter, stop processing (going idle) after 100th error :
|
||||
self._errors += 1
|
||||
# sleep a little bit (to get around time-related errors):
|
||||
@@ -998,9 +1031,6 @@ class FileFilter(Filter):
|
||||
log = self.__logs.pop(path)
|
||||
except KeyError:
|
||||
return
|
||||
db = self.jail.database
|
||||
if db is not None:
|
||||
db.updateLog(self.jail, log)
|
||||
logSys.info("Removed logfile: %r", path)
|
||||
self._delLogPath(path)
|
||||
return
|
||||
@@ -1064,6 +1094,7 @@ class FileFilter(Filter):
|
||||
# is created and is added to the FailManager.
|
||||
|
||||
def getFailures(self, filename, inOperation=None):
|
||||
if self.idle: return False
|
||||
log = self.getLog(filename)
|
||||
if log is None:
|
||||
logSys.error("Unable to get failures in %s", filename)
|
||||
@@ -1109,19 +1140,25 @@ class FileFilter(Filter):
|
||||
while not self.idle:
|
||||
line = log.readline()
|
||||
if not self.active: break; # jail has been stopped
|
||||
if not line:
|
||||
if line is None:
|
||||
# The jail reached the bottom, simply set in operation for this log
|
||||
# (since we are first time at end of file, growing is only possible after modifications):
|
||||
log.inOperation = True
|
||||
break
|
||||
# acquire in operation from log and process:
|
||||
self.inOperation = inOperation if inOperation is not None else log.inOperation
|
||||
self.processLineAndAdd(line.rstrip('\r\n'))
|
||||
self.processLineAndAdd(line)
|
||||
finally:
|
||||
log.close()
|
||||
db = self.jail.database
|
||||
if db is not None:
|
||||
db.updateLog(self.jail, log)
|
||||
if self.jail.database is not None:
|
||||
self._pendDBUpdates[log] = 1
|
||||
if (
|
||||
self.ticks % 100 == 0
|
||||
or MyTime.time() >= self._nextUpdateTM
|
||||
or not self.active
|
||||
):
|
||||
self._updateDBPending()
|
||||
self._nextUpdateTM = MyTime.time() + Utils.DEFAULT_SLEEP_TIME * 5
|
||||
return True
|
||||
|
||||
##
|
||||
@@ -1132,7 +1169,9 @@ class FileFilter(Filter):
|
||||
fs = container.getFileSize()
|
||||
if logSys.getEffectiveLevel() <= logging.DEBUG:
|
||||
logSys.debug("Seek to find time %s (%s), file size %s", date,
|
||||
datetime.datetime.fromtimestamp(date).strftime("%Y-%m-%d %H:%M:%S"), fs)
|
||||
MyTime.time2str(date), fs)
|
||||
if not fs:
|
||||
return
|
||||
minp = container.getPos()
|
||||
maxp = fs
|
||||
tryPos = minp
|
||||
@@ -1156,8 +1195,8 @@ class FileFilter(Filter):
|
||||
dateTimeMatch = None
|
||||
nextp = None
|
||||
while True:
|
||||
line = container.readline()
|
||||
if not line:
|
||||
line = container.readline(False)
|
||||
if line is None:
|
||||
break
|
||||
(timeMatch, template) = self.dateDetector.matchTime(line)
|
||||
if timeMatch:
|
||||
@@ -1211,7 +1250,7 @@ class FileFilter(Filter):
|
||||
container.setPos(foundPos)
|
||||
if logSys.getEffectiveLevel() <= logging.DEBUG:
|
||||
logSys.debug("Position %s from %s, found time %s (%s) within %s seeks", lastPos, fs, foundTime,
|
||||
(datetime.datetime.fromtimestamp(foundTime).strftime("%Y-%m-%d %H:%M:%S") if foundTime is not None else ''), cntr)
|
||||
(MyTime.time2str(foundTime) if foundTime is not None else ''), cntr)
|
||||
|
||||
def status(self, flavor="basic"):
|
||||
"""Status of Filter plus files being monitored.
|
||||
@@ -1221,12 +1260,33 @@ class FileFilter(Filter):
|
||||
ret.append(("File list", path))
|
||||
return ret
|
||||
|
||||
def stop(self):
|
||||
"""Stop monitoring of log-file(s)
|
||||
def _updateDBPending(self):
|
||||
"""Apply pending updates (log position) to database.
|
||||
"""
|
||||
db = self.jail.database
|
||||
while True:
|
||||
try:
|
||||
log, args = self._pendDBUpdates.popitem()
|
||||
except KeyError:
|
||||
break
|
||||
db.updateLog(self.jail, log)
|
||||
|
||||
def onStop(self):
|
||||
"""Stop monitoring of log-file(s). Invoked after run method.
|
||||
"""
|
||||
# ensure positions of pending logs are up-to-date:
|
||||
if self._pendDBUpdates and self.jail.database:
|
||||
self._updateDBPending()
|
||||
# stop files monitoring:
|
||||
for path in self.__logs.keys():
|
||||
self.delLogPath(path)
|
||||
|
||||
def stop(self):
|
||||
"""Stop filter
|
||||
"""
|
||||
# normally onStop will be called automatically in thread after its run ends,
|
||||
# but for backwards compatibilities we'll invoke it in caller of stop method.
|
||||
self.onStop()
|
||||
# stop thread:
|
||||
super(Filter, self).stop()
|
||||
|
||||
@@ -1254,34 +1314,56 @@ except ImportError: # pragma: no cover
|
||||
|
||||
class FileContainer:
|
||||
|
||||
def __init__(self, filename, encoding, tail=False):
|
||||
def __init__(self, filename, encoding, tail=False, doOpen=False):
|
||||
self.__filename = filename
|
||||
self.waitForLineEnd = True
|
||||
self.setEncoding(encoding)
|
||||
self.__tail = tail
|
||||
self.__handler = None
|
||||
self.__pos = 0
|
||||
self.__pos4hash = 0
|
||||
self.__hash = ''
|
||||
self.__hashNextTime = time.time() + 30
|
||||
# Try to open the file. Raises an exception if an error occurred.
|
||||
handler = open(filename, 'rb')
|
||||
stats = os.fstat(handler.fileno())
|
||||
self.__ino = stats.st_ino
|
||||
if doOpen: # fail2ban-regex only (don't need to reopen it and check for rotation)
|
||||
self.__handler = handler
|
||||
return
|
||||
try:
|
||||
firstLine = handler.readline()
|
||||
# Computes the MD5 of the first line.
|
||||
self.__hash = md5sum(firstLine).hexdigest()
|
||||
# Start at the beginning of file if tail mode is off.
|
||||
if tail:
|
||||
handler.seek(0, 2)
|
||||
self.__pos = handler.tell()
|
||||
else:
|
||||
self.__pos = 0
|
||||
stats = os.fstat(handler.fileno())
|
||||
self.__ino = stats.st_ino
|
||||
if stats.st_size:
|
||||
firstLine = handler.readline()
|
||||
# first line available and contains new-line:
|
||||
if firstLine != firstLine.rstrip(b'\r\n'):
|
||||
# Computes the MD5 of the first line.
|
||||
self.__hash = md5sum(firstLine).hexdigest()
|
||||
# if tail mode scroll to the end of file
|
||||
if tail:
|
||||
handler.seek(0, 2)
|
||||
self.__pos = handler.tell()
|
||||
finally:
|
||||
handler.close()
|
||||
## shows that log is in operation mode (expecting new messages only from here):
|
||||
self.inOperation = tail
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.__filename)
|
||||
def __eq__(self, other):
|
||||
return (id(self) == id(other) or
|
||||
self.__filename == (other.__filename if isinstance(other, FileContainer) else other)
|
||||
)
|
||||
def __repr__(self):
|
||||
return 'file-log:'+self.__filename
|
||||
|
||||
def getFileName(self):
|
||||
return self.__filename
|
||||
|
||||
def getFileSize(self):
|
||||
h = self.__handler
|
||||
if h is not None:
|
||||
stats = os.fstat(h.fileno())
|
||||
return stats.st_size
|
||||
return os.path.getsize(self.__filename);
|
||||
|
||||
def setEncoding(self, encoding):
|
||||
@@ -1300,38 +1382,54 @@ class FileContainer:
|
||||
def setPos(self, value):
|
||||
self.__pos = value
|
||||
|
||||
def open(self):
|
||||
self.__handler = open(self.__filename, 'rb')
|
||||
# Set the file descriptor to be FD_CLOEXEC
|
||||
fd = self.__handler.fileno()
|
||||
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
|
||||
# Stat the file before even attempting to read it
|
||||
stats = os.fstat(self.__handler.fileno())
|
||||
if not stats.st_size:
|
||||
# yoh: so it is still an empty file -- nothing should be
|
||||
# read from it yet
|
||||
# print "D: no content -- return"
|
||||
return False
|
||||
firstLine = self.__handler.readline()
|
||||
# Computes the MD5 of the first line.
|
||||
myHash = md5sum(firstLine).hexdigest()
|
||||
## print "D: fn=%s hashes=%s/%s inos=%s/%s pos=%s rotate=%s" % (
|
||||
## self.__filename, self.__hash, myHash, stats.st_ino, self.__ino, self.__pos,
|
||||
## self.__hash != myHash or self.__ino != stats.st_ino)
|
||||
## sys.stdout.flush()
|
||||
# Compare hash and inode
|
||||
if self.__hash != myHash or self.__ino != stats.st_ino:
|
||||
logSys.log(logging.MSG, "Log rotation detected for %s", self.__filename)
|
||||
self.__hash = myHash
|
||||
self.__ino = stats.st_ino
|
||||
self.__pos = 0
|
||||
# Sets the file pointer to the last position.
|
||||
self.__handler.seek(self.__pos)
|
||||
def open(self, forcePos=None):
|
||||
h = open(self.__filename, 'rb')
|
||||
try:
|
||||
# Set the file descriptor to be FD_CLOEXEC
|
||||
fd = h.fileno()
|
||||
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
|
||||
myHash = self.__hash
|
||||
# Stat the file before even attempting to read it
|
||||
stats = os.fstat(h.fileno())
|
||||
rotflg = stats.st_size < self.__pos or stats.st_ino != self.__ino
|
||||
if rotflg or not len(myHash) or time.time() > self.__hashNextTime:
|
||||
myHash = ''
|
||||
firstLine = h.readline()
|
||||
# Computes the MD5 of the first line (if it is complete)
|
||||
if firstLine != firstLine.rstrip(b'\r\n'):
|
||||
myHash = md5sum(firstLine).hexdigest()
|
||||
self.__hashNextTime = time.time() + 30
|
||||
elif stats.st_size == self.__pos:
|
||||
myHash = self.__hash
|
||||
# Compare size, hash and inode
|
||||
if rotflg or myHash != self.__hash:
|
||||
if self.__hash != '':
|
||||
logSys.log(logging.MSG, "Log rotation detected for %s, reason: %r", self.__filename,
|
||||
(stats.st_size, self.__pos, stats.st_ino, self.__ino, myHash, self.__hash))
|
||||
self.__ino = stats.st_ino
|
||||
self.__pos = 0
|
||||
self.__hash = myHash
|
||||
# if nothing to read from file yet (empty or no new data):
|
||||
if forcePos is not None:
|
||||
self.__pos = forcePos
|
||||
elif stats.st_size <= self.__pos:
|
||||
return False
|
||||
# Sets the file pointer to the last position.
|
||||
h.seek(self.__pos)
|
||||
# leave file open (to read content):
|
||||
self.__handler = h; h = None
|
||||
finally:
|
||||
# close (no content or error only)
|
||||
if h:
|
||||
h.close(); h = None
|
||||
return True
|
||||
|
||||
def seek(self, offs, endLine=True):
|
||||
h = self.__handler
|
||||
if h is None:
|
||||
self.open(offs)
|
||||
h = self.__handler
|
||||
# seek to given position
|
||||
h.seek(offs, 0)
|
||||
# goto end of next line
|
||||
@@ -1349,6 +1447,9 @@ class FileContainer:
|
||||
try:
|
||||
return line.decode(enc, 'strict')
|
||||
except (UnicodeDecodeError, UnicodeEncodeError) as e:
|
||||
# avoid warning if got incomplete end of line (e. g. '\n' in "...[0A" followed by "00]..." for utf-16le:
|
||||
if (e.end == len(line) and line[e.start] in b'\r\n'):
|
||||
return line[0:e.start].decode(enc, 'replace')
|
||||
global _decode_line_warn
|
||||
lev = 7
|
||||
if not _decode_line_warn.get(filename, 0):
|
||||
@@ -1357,29 +1458,85 @@ class FileContainer:
|
||||
logSys.log(lev,
|
||||
"Error decoding line from '%s' with '%s'.", filename, enc)
|
||||
if logSys.getEffectiveLevel() <= lev:
|
||||
logSys.log(lev, "Consider setting logencoding=utf-8 (or another appropriate"
|
||||
" encoding) for this jail. Continuing"
|
||||
" to process line ignoring invalid characters: %r",
|
||||
logSys.log(lev,
|
||||
"Consider setting logencoding to appropriate encoding for this jail. "
|
||||
"Continuing to process line ignoring invalid characters: %r",
|
||||
line)
|
||||
# decode with replacing error chars:
|
||||
line = line.decode(enc, 'replace')
|
||||
return line
|
||||
|
||||
def readline(self):
|
||||
def readline(self, complete=True):
|
||||
"""Read line from file
|
||||
|
||||
In opposite to pythons readline it doesn't return new-line,
|
||||
so returns either the line if line is complete (and complete=True) or None
|
||||
if line is not complete (and complete=True) or there is no content to read.
|
||||
If line is complete (and complete is True), it also shift current known
|
||||
position to begin of next line.
|
||||
|
||||
Also it is safe against interim new-line bytes (e. g. part of multi-byte char)
|
||||
in given encoding.
|
||||
"""
|
||||
if self.__handler is None:
|
||||
return ""
|
||||
return FileContainer.decode_line(
|
||||
self.getFileName(), self.getEncoding(), self.__handler.readline())
|
||||
# read raw bytes up to \n char:
|
||||
b = self.__handler.readline()
|
||||
if not b:
|
||||
return None
|
||||
bl = len(b)
|
||||
# convert to log-encoding (new-line char could disappear if it is part of multi-byte sequence):
|
||||
r = FileContainer.decode_line(
|
||||
self.getFileName(), self.getEncoding(), b)
|
||||
# trim new-line at end and check the line was written complete (contains a new-line):
|
||||
l = r.rstrip('\r\n')
|
||||
if complete:
|
||||
if l == r:
|
||||
# try to fill buffer in order to find line-end in log encoding:
|
||||
fnd = 0
|
||||
while 1:
|
||||
r = self.__handler.readline()
|
||||
if not r:
|
||||
break
|
||||
b += r
|
||||
bl += len(r)
|
||||
# convert to log-encoding:
|
||||
r = FileContainer.decode_line(
|
||||
self.getFileName(), self.getEncoding(), b)
|
||||
# ensure new-line is not in the middle (buffered 2 strings, e. g. in utf-16le it is "...[0A"+"00]..."):
|
||||
e = r.find('\n')
|
||||
if e >= 0 and e != len(r)-1:
|
||||
l, r = r[0:e], r[0:e+1]
|
||||
# back to bytes and get offset to seek after NL:
|
||||
r = r.encode(self.getEncoding(), 'replace')
|
||||
self.__handler.seek(-bl+len(r), 1)
|
||||
return l
|
||||
# trim new-line at end and check the line was written complete (contains a new-line):
|
||||
l = r.rstrip('\r\n')
|
||||
if l != r:
|
||||
return l
|
||||
if self.waitForLineEnd:
|
||||
# not fulfilled - seek back and return:
|
||||
self.__handler.seek(-bl, 1)
|
||||
return None
|
||||
return l
|
||||
|
||||
def close(self):
|
||||
if not self.__handler is None:
|
||||
# Saves the last position.
|
||||
if self.__handler is not None:
|
||||
# Saves the last real position.
|
||||
self.__pos = self.__handler.tell()
|
||||
# Closes the file.
|
||||
self.__handler.close()
|
||||
self.__handler = None
|
||||
## print "D: Closed %s with pos %d" % (handler, self.__pos)
|
||||
## sys.stdout.flush()
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
def next(self):
|
||||
line = self.readline()
|
||||
if line is None:
|
||||
self.close()
|
||||
raise StopIteration
|
||||
return line
|
||||
|
||||
_decode_line_warn = Utils.Cache(maxCount=1000, maxTime=24*60*60);
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@ class FilterGamin(FileFilter):
|
||||
|
||||
def __init__(self, jail):
|
||||
FileFilter.__init__(self, jail)
|
||||
self.__modified = False
|
||||
# Gamin monitor
|
||||
self.monitor = gamin.WatchMonitor()
|
||||
fd = self.monitor.get_fd()
|
||||
@@ -67,21 +66,9 @@ class FilterGamin(FileFilter):
|
||||
logSys.log(4, "Got event: " + repr(event) + " for " + path)
|
||||
if event in (gamin.GAMCreated, gamin.GAMChanged, gamin.GAMExists):
|
||||
logSys.debug("File changed: " + path)
|
||||
self.__modified = True
|
||||
|
||||
self.ticks += 1
|
||||
self._process_file(path)
|
||||
|
||||
def _process_file(self, path):
|
||||
"""Process a given file
|
||||
|
||||
TODO -- RF:
|
||||
this is a common logic and must be shared/provided by FileFilter
|
||||
"""
|
||||
self.getFailures(path)
|
||||
if not self.banASAP: # pragma: no cover
|
||||
self.performBan()
|
||||
self.__modified = False
|
||||
|
||||
##
|
||||
# Add a log file path
|
||||
@@ -128,6 +115,9 @@ class FilterGamin(FileFilter):
|
||||
Utils.wait_for(lambda: not self.active or self._handleEvents(),
|
||||
self.sleeptime)
|
||||
self.ticks += 1
|
||||
if self.ticks % 10 == 0:
|
||||
self.performSvc()
|
||||
|
||||
logSys.debug("[%s] filter terminated", self.jailName)
|
||||
return True
|
||||
|
||||
|
||||
@@ -27,9 +27,7 @@ __license__ = "GPL"
|
||||
import os
|
||||
import time
|
||||
|
||||
from .failmanager import FailManagerEmpty
|
||||
from .filter import FileFilter
|
||||
from .mytime import MyTime
|
||||
from .utils import Utils
|
||||
from ..helpers import getLogger, logging
|
||||
|
||||
@@ -55,7 +53,6 @@ class FilterPoll(FileFilter):
|
||||
|
||||
def __init__(self, jail):
|
||||
FileFilter.__init__(self, jail)
|
||||
self.__modified = False
|
||||
## The time of the last modification of the file.
|
||||
self.__prevStats = dict()
|
||||
self.__file404Cnt = dict()
|
||||
@@ -115,20 +112,17 @@ class FilterPoll(FileFilter):
|
||||
break
|
||||
for filename in modlst:
|
||||
self.getFailures(filename)
|
||||
self.__modified = True
|
||||
|
||||
self.ticks += 1
|
||||
if self.__modified:
|
||||
if not self.banASAP: # pragma: no cover
|
||||
self.performBan()
|
||||
self.__modified = False
|
||||
if self.ticks % 10 == 0:
|
||||
self.performSvc()
|
||||
except Exception as e: # pragma: no cover
|
||||
if not self.active: # if not active - error by stop...
|
||||
break
|
||||
logSys.error("Caught unhandled exception in main cycle: %r", e,
|
||||
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
# incr common error counter:
|
||||
self.commonError()
|
||||
self.commonError("unhandled", e)
|
||||
logSys.debug("[%s] filter terminated", self.jailName)
|
||||
return True
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ class FilterPyinotify(FileFilter):
|
||||
|
||||
def __init__(self, jail):
|
||||
FileFilter.__init__(self, jail)
|
||||
self.__modified = False
|
||||
# Pyinotify watch manager
|
||||
self.__monitor = pyinotify.WatchManager()
|
||||
self.__notifier = None
|
||||
@@ -140,9 +139,6 @@ class FilterPyinotify(FileFilter):
|
||||
"""
|
||||
if not self.idle:
|
||||
self.getFailures(path)
|
||||
if not self.banASAP: # pragma: no cover
|
||||
self.performBan()
|
||||
self.__modified = False
|
||||
|
||||
def _addPending(self, path, reason, isDir=False):
|
||||
if path not in self.__pending:
|
||||
@@ -169,7 +165,7 @@ class FilterPyinotify(FileFilter):
|
||||
return
|
||||
found = {}
|
||||
minTime = 60
|
||||
for path, (retardTM, isDir) in self.__pending.iteritems():
|
||||
for path, (retardTM, isDir) in list(self.__pending.items()):
|
||||
if ntm - self.__pendingChkTime < retardTM:
|
||||
if minTime > retardTM: minTime = retardTM
|
||||
continue
|
||||
@@ -192,7 +188,7 @@ class FilterPyinotify(FileFilter):
|
||||
self._refreshWatcher(path, isDir=isDir)
|
||||
if isDir:
|
||||
# check all files belong to this dir:
|
||||
for logpath in self.__watchFiles:
|
||||
for logpath in list(self.__watchFiles):
|
||||
if logpath.startswith(path + pathsep):
|
||||
# if still no file - add to pending, otherwise refresh and process:
|
||||
if not os.path.isfile(logpath):
|
||||
@@ -272,15 +268,13 @@ class FilterPyinotify(FileFilter):
|
||||
|
||||
def _addLogPath(self, path):
|
||||
self._addFileWatcher(path)
|
||||
# initial scan:
|
||||
# notify (wake up if in waiting):
|
||||
if self.active:
|
||||
# we can execute it right now:
|
||||
self._process_file(path)
|
||||
else:
|
||||
# retard until filter gets started, isDir=None signals special case: process file only (don't need to refresh monitor):
|
||||
self._addPending(path, ('INITIAL', path), isDir=None)
|
||||
self.__pendingMinTime = 0
|
||||
# retard until filter gets started, isDir=None signals special case: process file only (don't need to refresh monitor):
|
||||
self._addPending(path, ('INITIAL', path), isDir=None)
|
||||
|
||||
##
|
||||
##
|
||||
# Delete a log path
|
||||
#
|
||||
# @param path the log file to delete
|
||||
@@ -291,7 +285,7 @@ class FilterPyinotify(FileFilter):
|
||||
logSys.error("Failed to remove watch on path: %s", path)
|
||||
|
||||
path_dir = dirname(path)
|
||||
for k in self.__watchFiles:
|
||||
for k in list(self.__watchFiles):
|
||||
if k.startswith(path_dir + pathsep):
|
||||
path_dir = None
|
||||
break
|
||||
@@ -345,16 +339,26 @@ class FilterPyinotify(FileFilter):
|
||||
self.__notifier.process_events()
|
||||
|
||||
# wait for events / timeout:
|
||||
notify_maxtout = self.__notify_maxtout
|
||||
def __check_events():
|
||||
return not self.active or self.__notifier.check_events(timeout=notify_maxtout)
|
||||
if Utils.wait_for(__check_events, min(self.sleeptime, self.__pendingMinTime)):
|
||||
return (
|
||||
not self.active
|
||||
or bool(self.__notifier.check_events(timeout=self.__notify_maxtout))
|
||||
or (self.__pendingMinTime and self.__pending)
|
||||
)
|
||||
wres = Utils.wait_for(__check_events, min(self.sleeptime, self.__pendingMinTime))
|
||||
if wres:
|
||||
if not self.active: break
|
||||
self.__notifier.read_events()
|
||||
if not isinstance(wres, dict):
|
||||
self.__notifier.read_events()
|
||||
|
||||
self.ticks += 1
|
||||
|
||||
# check pending files/dirs (logrotate ready):
|
||||
if not self.idle:
|
||||
self._checkPending()
|
||||
if self.idle:
|
||||
continue
|
||||
self._checkPending()
|
||||
if self.ticks % 10 == 0:
|
||||
self.performSvc()
|
||||
|
||||
except Exception as e: # pragma: no cover
|
||||
if not self.active: # if not active - error by stop...
|
||||
@@ -362,10 +366,8 @@ class FilterPyinotify(FileFilter):
|
||||
logSys.error("Caught unhandled exception in main cycle: %r", e,
|
||||
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
# incr common error counter:
|
||||
self.commonError()
|
||||
self.commonError("unhandled", e)
|
||||
|
||||
self.ticks += 1
|
||||
|
||||
logSys.debug("[%s] filter exited (pyinotifier)", self.jailName)
|
||||
self.__notifier = None
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ __copyright__ = "Copyright (c) 2013 Steven Hiscocks"
|
||||
__license__ = "GPL"
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import time
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
@@ -91,9 +92,14 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
|
||||
try:
|
||||
args['flags'] = int(kwargs.pop('journalflags'))
|
||||
except KeyError:
|
||||
# be sure all journal types will be opened if files specified (don't set flags):
|
||||
if 'files' not in args or not len(args['files']):
|
||||
args['flags'] = 4
|
||||
# be sure all journal types will be opened if files/path specified (don't set flags):
|
||||
if ('files' not in args or not len(args['files'])) and ('path' not in args or not args['path']):
|
||||
args['flags'] = int(os.getenv("F2B_SYSTEMD_DEFAULT_FLAGS", 4))
|
||||
|
||||
try:
|
||||
args['namespace'] = kwargs.pop('namespace')
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return args
|
||||
|
||||
@@ -252,6 +258,10 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
|
||||
date = datetime.datetime.fromtimestamp(date)
|
||||
self.__journal.seek_realtime(date)
|
||||
|
||||
def inOperationMode(self):
|
||||
self.inOperation = True
|
||||
logSys.info("[%s] Jail is in operation now (process new journal entries)", self.jailName)
|
||||
|
||||
##
|
||||
# Main loop.
|
||||
#
|
||||
@@ -262,24 +272,46 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
|
||||
|
||||
if not self.getJournalMatch():
|
||||
logSys.notice(
|
||||
"Jail started without 'journalmatch' set. "
|
||||
"[%s] Jail started without 'journalmatch' set. "
|
||||
"Jail regexs will be checked against all journal entries, "
|
||||
"which is not advised for performance reasons.")
|
||||
"which is not advised for performance reasons.", self.jailName)
|
||||
|
||||
# Try to obtain the last known time (position of journal)
|
||||
start_time = 0
|
||||
if self.jail.database is not None:
|
||||
start_time = self.jail.database.getJournalPos(self.jail, 'systemd-journal') or 0
|
||||
# Seek to max(last_known_time, now - findtime) in journal
|
||||
start_time = max( start_time, MyTime.time() - int(self.getFindTime()) )
|
||||
self.seekToTime(start_time)
|
||||
# Move back one entry to ensure do not end up in dead space
|
||||
# if start time beyond end of journal
|
||||
# Save current cursor position (to recognize in operation mode):
|
||||
logentry = None
|
||||
try:
|
||||
self.__journal.get_previous()
|
||||
self.__journal.seek_tail()
|
||||
logentry = self.__journal.get_previous()
|
||||
self.__journal.get_next()
|
||||
except OSError:
|
||||
pass # Reading failure, so safe to ignore
|
||||
logentry = None # Reading failure, so safe to ignore
|
||||
if logentry:
|
||||
# Try to obtain the last known time (position of journal)
|
||||
startTime = 0
|
||||
if self.jail.database is not None:
|
||||
startTime = self.jail.database.getJournalPos(self.jail, 'systemd-journal') or 0
|
||||
# Seek to max(last_known_time, now - findtime) in journal
|
||||
startTime = max( startTime, MyTime.time() - int(self.getFindTime()) )
|
||||
self.seekToTime(startTime)
|
||||
# Not in operation while we'll read old messages ...
|
||||
self.inOperation = False
|
||||
# Save current time in order to check time to switch "in operation" mode
|
||||
startTime = (1, MyTime.time(), logentry.get('__CURSOR'))
|
||||
# Move back one entry to ensure do not end up in dead space
|
||||
# if start time beyond end of journal
|
||||
try:
|
||||
self.__journal.get_previous()
|
||||
except OSError:
|
||||
pass # Reading failure, so safe to ignore
|
||||
else:
|
||||
# empty journal or no entries for current filter:
|
||||
self.inOperationMode()
|
||||
# seek_tail() seems to have a bug by no entries (could bypass some entries hereafter), so seek to now instead:
|
||||
startTime = MyTime.time()
|
||||
self.seekToTime(startTime)
|
||||
# for possible future switches of in-operation mode:
|
||||
startTime = (0, startTime)
|
||||
|
||||
line = None
|
||||
while self.active:
|
||||
# wait for records (or for timeout in sleeptime seconds):
|
||||
try:
|
||||
@@ -289,9 +321,10 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
|
||||
#self.__journal.wait(self.sleeptime) != journal.NOP
|
||||
##
|
||||
## wait for entries without sleep in intervals, because "sleeping" in journal.wait:
|
||||
Utils.wait_for(lambda: not self.active or \
|
||||
self.__journal.wait(Utils.DEFAULT_SLEEP_INTERVAL) != journal.NOP,
|
||||
self.sleeptime, 0.00001)
|
||||
if not logentry:
|
||||
Utils.wait_for(lambda: not self.active or \
|
||||
self.__journal.wait(Utils.DEFAULT_SLEEP_INTERVAL) != journal.NOP,
|
||||
self.sleeptime, 0.00001)
|
||||
if self.idle:
|
||||
# because journal.wait will returns immediatelly if we have records in journal,
|
||||
# just wait a little bit here for not idle, to prevent hi-load:
|
||||
@@ -310,27 +343,50 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
|
||||
e, exc_info=logSys.getEffectiveLevel() <= logging.DEBUG)
|
||||
self.ticks += 1
|
||||
if logentry:
|
||||
line = self.formatJournalEntry(logentry)
|
||||
self.processLineAndAdd(*line)
|
||||
line, tm = self.formatJournalEntry(logentry)
|
||||
# switch "in operation" mode if we'll find start entry (+ some delta):
|
||||
if not self.inOperation:
|
||||
if tm >= MyTime.time() - 1: # reached now (approximated):
|
||||
self.inOperationMode()
|
||||
elif startTime[0] == 1:
|
||||
# if it reached start entry (or get read time larger than start time)
|
||||
if logentry.get('__CURSOR') == startTime[2] or tm > startTime[1]:
|
||||
# give the filter same time it needed to reach the start entry:
|
||||
startTime = (0, MyTime.time()*2 - startTime[1])
|
||||
elif tm > startTime[1]: # reached start time (approximated):
|
||||
self.inOperationMode()
|
||||
# process line
|
||||
self.processLineAndAdd(line, tm)
|
||||
self.__modified += 1
|
||||
if self.__modified >= 100: # todo: should be configurable
|
||||
break
|
||||
else:
|
||||
# "in operation" mode since we don't have messages anymore (reached end of journal):
|
||||
if not self.inOperation:
|
||||
self.inOperationMode()
|
||||
break
|
||||
if self.__modified:
|
||||
if not self.banASAP: # pragma: no cover
|
||||
self.performBan()
|
||||
self.__modified = 0
|
||||
# update position in log (time and iso string):
|
||||
if self.jail.database is not None:
|
||||
self.jail.database.updateJournal(self.jail, 'systemd-journal', line[1], line[0][1])
|
||||
self.__modified = 0
|
||||
if self.ticks % 10 == 0:
|
||||
self.performSvc()
|
||||
# update position in log (time and iso string):
|
||||
if self.jail.database:
|
||||
if line:
|
||||
self._pendDBUpdates['systemd-journal'] = (tm, line[1])
|
||||
line = None
|
||||
if self._pendDBUpdates and (
|
||||
self.ticks % 100 == 0
|
||||
or MyTime.time() >= self._nextUpdateTM
|
||||
or not self.active
|
||||
):
|
||||
self._updateDBPending()
|
||||
self._nextUpdateTM = MyTime.time() + Utils.DEFAULT_SLEEP_TIME * 5
|
||||
except Exception as e: # pragma: no cover
|
||||
if not self.active: # if not active - error by stop...
|
||||
break
|
||||
logSys.error("Caught unhandled exception in main cycle: %r", e,
|
||||
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
# incr common error counter:
|
||||
self.commonError()
|
||||
self.commonError("unhandled", e)
|
||||
|
||||
logSys.debug("[%s] filter terminated", self.jailName)
|
||||
|
||||
@@ -350,3 +406,22 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
|
||||
ret.append(("Journal matches",
|
||||
[" + ".join(" ".join(match) for match in self.__matches)]))
|
||||
return ret
|
||||
|
||||
def _updateDBPending(self):
|
||||
"""Apply pending updates (jornal position) to database.
|
||||
"""
|
||||
db = self.jail.database
|
||||
while True:
|
||||
try:
|
||||
log, args = self._pendDBUpdates.popitem()
|
||||
except KeyError:
|
||||
break
|
||||
db.updateJournal(self.jail, log, *args)
|
||||
|
||||
def onStop(self):
|
||||
"""Stop monitoring of journal. Invoked after run method.
|
||||
"""
|
||||
# ensure positions of pending logs are up-to-date:
|
||||
if self._pendDBUpdates and self.jail.database:
|
||||
self._updateDBPending()
|
||||
|
||||
|
||||
+32
-11
@@ -169,27 +169,31 @@ class DNSUtils:
|
||||
DNSUtils.CACHE_ipToName.set(key, name)
|
||||
return name
|
||||
|
||||
# key find cached own hostnames (this tuple-key cannot be used elsewhere):
|
||||
_getSelfNames_key = ('self','dns')
|
||||
|
||||
@staticmethod
|
||||
def getSelfNames():
|
||||
"""Get own host names of self"""
|
||||
# try find cached own hostnames (this tuple-key cannot be used elsewhere):
|
||||
key = ('self','dns')
|
||||
names = DNSUtils.CACHE_ipToName.get(key)
|
||||
# try find cached own hostnames:
|
||||
names = DNSUtils.CACHE_ipToName.get(DNSUtils._getSelfNames_key)
|
||||
# get it using different ways (a set with names of localhost, hostname, fully qualified):
|
||||
if names is None:
|
||||
names = set([
|
||||
'localhost', DNSUtils.getHostname(False), DNSUtils.getHostname(True)
|
||||
]) - set(['']) # getHostname can return ''
|
||||
# cache and return :
|
||||
DNSUtils.CACHE_ipToName.set(key, names)
|
||||
DNSUtils.CACHE_ipToName.set(DNSUtils._getSelfNames_key, names)
|
||||
return names
|
||||
|
||||
# key to find cached own IPs (this tuple-key cannot be used elsewhere):
|
||||
_getSelfIPs_key = ('self','ips')
|
||||
|
||||
@staticmethod
|
||||
def getSelfIPs():
|
||||
"""Get own IP addresses of self"""
|
||||
# try find cached own IPs (this tuple-key cannot be used elsewhere):
|
||||
key = ('self','ips')
|
||||
ips = DNSUtils.CACHE_nameToIp.get(key)
|
||||
# to find cached own IPs:
|
||||
ips = DNSUtils.CACHE_nameToIp.get(DNSUtils._getSelfIPs_key)
|
||||
# get it using different ways (a set with IPs of localhost, hostname, fully qualified):
|
||||
if ips is None:
|
||||
ips = set()
|
||||
@@ -199,13 +203,30 @@ class DNSUtils:
|
||||
except Exception as e: # pragma: no cover
|
||||
logSys.warning("Retrieving own IPs of %s failed: %s", hostname, e)
|
||||
# cache and return :
|
||||
DNSUtils.CACHE_nameToIp.set(key, ips)
|
||||
DNSUtils.CACHE_nameToIp.set(DNSUtils._getSelfIPs_key, ips)
|
||||
return ips
|
||||
|
||||
_IPv6IsAllowed = None
|
||||
|
||||
@staticmethod
|
||||
def setIPv6IsAllowed(value):
|
||||
DNSUtils._IPv6IsAllowed = value
|
||||
logSys.debug("IPv6 is %s", ('on' if value else 'off') if value is not None else 'auto')
|
||||
return value
|
||||
|
||||
# key to find cached value of IPv6 allowance (this tuple-key cannot be used elsewhere):
|
||||
_IPv6IsAllowed_key = ('self','ipv6-allowed')
|
||||
|
||||
@staticmethod
|
||||
def IPv6IsAllowed():
|
||||
# return os.path.exists("/proc/net/if_inet6") || any((':' in ip) for ip in DNSUtils.getSelfIPs())
|
||||
return any((':' in ip.ntoa) for ip in DNSUtils.getSelfIPs())
|
||||
if DNSUtils._IPv6IsAllowed is not None:
|
||||
return DNSUtils._IPv6IsAllowed
|
||||
v = DNSUtils.CACHE_nameToIp.get(DNSUtils._IPv6IsAllowed_key)
|
||||
if v is not None:
|
||||
return v
|
||||
v = any((':' in ip.ntoa) for ip in DNSUtils.getSelfIPs())
|
||||
DNSUtils.CACHE_nameToIp.set(DNSUtils._IPv6IsAllowed_key, v)
|
||||
return v
|
||||
|
||||
|
||||
##
|
||||
@@ -337,7 +358,7 @@ class IPAddr(object):
|
||||
return repr(self.ntoa)
|
||||
|
||||
def __str__(self):
|
||||
return self.ntoa
|
||||
return self.ntoa if isinstance(self.ntoa, basestring) else str(self.ntoa)
|
||||
|
||||
def __reduce__(self):
|
||||
"""IPAddr pickle-handler, that simply wraps IPAddr to the str
|
||||
|
||||
+83
-10
@@ -24,10 +24,13 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Lee Clemens, 2012 Y
|
||||
__license__ = "GPL"
|
||||
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
import Queue
|
||||
|
||||
from .actions import Actions
|
||||
from ..helpers import getLogger, extractOptions, MyTime
|
||||
from ..helpers import getLogger, _as_bool, extractOptions, MyTime
|
||||
from .mytime import MyTime
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = getLogger(__name__)
|
||||
@@ -75,6 +78,8 @@ class Jail(object):
|
||||
self.__name = name
|
||||
self.__queue = Queue.Queue()
|
||||
self.__filter = None
|
||||
# Extra parameters for increase ban time
|
||||
self._banExtra = {};
|
||||
logSys.info("Creating new jail '%s'" % self.name)
|
||||
if backend is not None:
|
||||
self._setBackend(backend)
|
||||
@@ -203,6 +208,8 @@ class Jail(object):
|
||||
Used by filter to add a failure for banning.
|
||||
"""
|
||||
self.__queue.put(ticket)
|
||||
# add ban to database moved to observer (should previously check not already banned
|
||||
# and increase ticket time if "bantime.increment" set)
|
||||
|
||||
def getFailTicket(self):
|
||||
"""Get a fail ticket from the jail.
|
||||
@@ -215,16 +222,80 @@ class Jail(object):
|
||||
except Queue.Empty:
|
||||
return False
|
||||
|
||||
def restoreCurrentBans(self):
|
||||
def setBanTimeExtra(self, opt, value):
|
||||
# merge previous extra with new option:
|
||||
be = self._banExtra;
|
||||
if value == '':
|
||||
value = None
|
||||
if value is not None:
|
||||
be[opt] = value;
|
||||
elif opt in be:
|
||||
del be[opt]
|
||||
logSys.info('Set banTime.%s = %s', opt, value)
|
||||
if opt == 'increment':
|
||||
be[opt] = _as_bool(value)
|
||||
if be.get(opt) and self.database is None:
|
||||
logSys.warning("ban time increment is not available as long jail database is not set")
|
||||
if opt in ['maxtime', 'rndtime']:
|
||||
if not value is None:
|
||||
be[opt] = MyTime.str2seconds(value)
|
||||
# prepare formula lambda:
|
||||
if opt in ['formula', 'factor', 'maxtime', 'rndtime', 'multipliers'] or be.get('evformula', None) is None:
|
||||
# split multifiers to an array begins with 0 (or empty if not set):
|
||||
if opt == 'multipliers':
|
||||
be['evmultipliers'] = [int(i) for i in (value.split(' ') if value is not None and value != '' else [])]
|
||||
# if we have multifiers - use it in lambda, otherwise compile and use formula within lambda
|
||||
multipliers = be.get('evmultipliers', [])
|
||||
banFactor = eval(be.get('factor', "1"))
|
||||
if len(multipliers):
|
||||
evformula = lambda ban, banFactor=banFactor: (
|
||||
ban.Time * banFactor * multipliers[ban.Count if ban.Count < len(multipliers) else -1]
|
||||
)
|
||||
else:
|
||||
formula = be.get('formula', 'ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor')
|
||||
formula = compile(formula, '~inline-conf-expr~', 'eval')
|
||||
evformula = lambda ban, banFactor=banFactor, formula=formula: max(ban.Time, eval(formula))
|
||||
# extend lambda with max time :
|
||||
if not be.get('maxtime', None) is None:
|
||||
maxtime = be['maxtime']
|
||||
evformula = lambda ban, evformula=evformula: min(evformula(ban), maxtime)
|
||||
# mix lambda with random time (to prevent bot-nets to calculate exact time IP can be unbanned):
|
||||
if not be.get('rndtime', None) is None:
|
||||
rndtime = be['rndtime']
|
||||
evformula = lambda ban, evformula=evformula: (evformula(ban) + random.random() * rndtime)
|
||||
# set to extra dict:
|
||||
be['evformula'] = evformula
|
||||
#logSys.info('banTimeExtra : %s' % json.dumps(be))
|
||||
|
||||
def getBanTimeExtra(self, opt=None):
|
||||
if opt is not None:
|
||||
return self._banExtra.get(opt, None)
|
||||
return self._banExtra
|
||||
|
||||
def getMaxBanTime(self):
|
||||
"""Returns max possible ban-time of jail.
|
||||
"""
|
||||
return self._banExtra.get("maxtime", -1) \
|
||||
if self._banExtra.get('increment') else self.actions.getBanTime()
|
||||
|
||||
def restoreCurrentBans(self, correctBanTime=True):
|
||||
"""Restore any previous valid bans from the database.
|
||||
"""
|
||||
try:
|
||||
if self.database is not None:
|
||||
forbantime = self.actions.getBanTime()
|
||||
for ticket in self.database.getCurrentBans(jail=self,
|
||||
forbantime=forbantime, maxmatches=self.filter.failManager.maxMatches):
|
||||
#logSys.debug('restored ticket: %s', ticket)
|
||||
if not self.filter.inIgnoreIPList(ticket.getIP(), log_ignore=True):
|
||||
if self._banExtra.get('increment'):
|
||||
forbantime = None;
|
||||
if correctBanTime:
|
||||
correctBanTime = self.getMaxBanTime()
|
||||
else:
|
||||
# use ban time as search time if we have not enabled a increasing:
|
||||
forbantime = self.actions.getBanTime()
|
||||
for ticket in self.database.getCurrentBans(jail=self, forbantime=forbantime,
|
||||
correctBanTime=correctBanTime, maxmatches=self.filter.failManager.maxMatches
|
||||
):
|
||||
try:
|
||||
#logSys.debug('restored ticket: %s', ticket)
|
||||
if self.filter.inIgnoreIPList(ticket.getIP(), log_ignore=True): continue
|
||||
# mark ticked was restored from database - does not put it again into db:
|
||||
ticket.restored = True
|
||||
# correct start time / ban time (by the same end of ban):
|
||||
@@ -235,11 +306,13 @@ class Jail(object):
|
||||
# ignore obsolete tickets:
|
||||
if btm != -1 and btm <= 0:
|
||||
continue
|
||||
ticket.setTime(MyTime.time())
|
||||
ticket.setBanTime(btm)
|
||||
self.putFailTicket(ticket)
|
||||
except Exception as e: # pragma: no cover
|
||||
logSys.error('Restore ticket failed: %s', e,
|
||||
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
except Exception as e: # pragma: no cover
|
||||
logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
logSys.error('Restore bans failed: %s', e,
|
||||
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
|
||||
def start(self):
|
||||
"""Start the jail, by starting filter and actions threads.
|
||||
|
||||
@@ -22,7 +22,10 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2013- Yaroslav Halchenko"
|
||||
__license__ = "GPL"
|
||||
|
||||
from threading import Lock
|
||||
from collections import Mapping
|
||||
try:
|
||||
from collections.abc import Mapping
|
||||
except ImportError:
|
||||
from collections import Mapping
|
||||
|
||||
from ..exceptions import DuplicateJailException, UnknownJailException
|
||||
from .jail import Jail
|
||||
|
||||
@@ -67,6 +67,8 @@ class JailThread(Thread):
|
||||
def run_with_except_hook(*args, **kwargs):
|
||||
try:
|
||||
run(*args, **kwargs)
|
||||
# call on stop callback to do some finalizations:
|
||||
self.onStop()
|
||||
except Exception as e:
|
||||
# avoid very sporadic error "'NoneType' object has no attribute 'exc_info'" (https://bugs.python.org/issue7336)
|
||||
# only extremely fast systems are affected ATM (2.7 / 3.x), if thread ends nothing is available here.
|
||||
@@ -97,6 +99,12 @@ class JailThread(Thread):
|
||||
self.active = True
|
||||
super(JailThread, self).start()
|
||||
|
||||
@abstractmethod
|
||||
def onStop(self): # pragma: no cover - absract
|
||||
"""Abstract - Called when thread ends (after run).
|
||||
"""
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
"""Sets `active` property to False, to flag run method to return.
|
||||
"""
|
||||
|
||||
@@ -113,6 +113,19 @@ class MyTime:
|
||||
return time.localtime(x)
|
||||
else:
|
||||
return time.localtime(MyTime.myTime)
|
||||
|
||||
@staticmethod
|
||||
def time2str(unixTime, format="%Y-%m-%d %H:%M:%S"):
|
||||
"""Convert time to a string representing as date and time using given format.
|
||||
Default format is ISO 8601, YYYY-MM-DD HH:MM:SS without microseconds.
|
||||
|
||||
@return ISO-capable string representation of given unixTime
|
||||
"""
|
||||
# consider end of 9999th year (in GMT+23 to avoid year overflow in other TZ)
|
||||
dt = datetime.datetime.fromtimestamp(
|
||||
unixTime).replace(microsecond=0
|
||||
) if unixTime < 253402214400 else datetime.datetime(9999, 12, 31, 23, 59, 59)
|
||||
return dt.strftime(format)
|
||||
|
||||
## precreate/precompile primitives used in str2seconds:
|
||||
|
||||
@@ -161,3 +174,62 @@ class MyTime:
|
||||
val = rexp.sub(rpl, val)
|
||||
val = MyTime._str2sec_fini.sub(r"\1+\2", val)
|
||||
return eval(val)
|
||||
|
||||
class seconds2str():
|
||||
"""Converts seconds to string on demand (if string representation needed).
|
||||
Ex: seconds2str(86400*390) = 1y 3w 4d
|
||||
seconds2str(86400*368) = 1y 3d
|
||||
seconds2str(86400*365.5) = 1y
|
||||
seconds2str(86400*2+3600*7+60*15) = 2d 7h 15m
|
||||
seconds2str(86400*2+3599) = 2d 1h
|
||||
seconds2str(3600-5) = 1h
|
||||
seconds2str(3600-10) = 59m 50s
|
||||
seconds2str(59) = 59s
|
||||
"""
|
||||
def __init__(self, sec):
|
||||
self.sec = sec
|
||||
def __str__(self):
|
||||
# s = str(datetime.timedelta(seconds=int(self.sec)))
|
||||
# return s if s[-3:] != ":00" else s[:-3]
|
||||
s = self.sec; c = 3
|
||||
# automatic accuracy: round by large values (and maximally 3 groups)
|
||||
if s >= 31536000: # a year as 365*24*60*60 (don't need to consider leap year by this accuracy)
|
||||
s = int(round(float(s)/86400)) # round by a day
|
||||
r = str(s//365) + 'y '; s %= 365
|
||||
if s >= 7:
|
||||
r += str(s//7) + 'w '; s %= 7
|
||||
if s:
|
||||
r += str(s) + 'd '
|
||||
return r[:-1]
|
||||
if s >= 604800: # a week as 24*60*60*7
|
||||
s = int(round(float(s)/3600)) # round by a hour
|
||||
r = str(s//168) + 'w '; s %= 168
|
||||
if s >= 24:
|
||||
r += str(s//24) + 'd '; s %= 24
|
||||
if s:
|
||||
r += str(s) + 'h '
|
||||
return r[:-1]
|
||||
if s >= 86400: # a day as 24*60*60
|
||||
s = int(round(float(s)/60)) # round by a minute
|
||||
r = str(s//1440) + 'd '; s %= 1440
|
||||
if s >= 60:
|
||||
r += str(s//60) + 'h '; s %= 60
|
||||
if s:
|
||||
r += str(s) + 'm '
|
||||
return r[:-1]
|
||||
if s >= 3595: # a hour as 60*60 (- 5 seconds)
|
||||
s = int(round(float(s)/10)) # round by 10 seconds
|
||||
r = str(s//360) + 'h '; s %= 360
|
||||
if s >= 6: # a minute
|
||||
r += str(s//6) + 'm '; s %= 6
|
||||
return r[:-1]
|
||||
r = ''
|
||||
if s >= 60: # a minute
|
||||
r += str(s//60) + 'm '; s %= 60
|
||||
if s: # remaining seconds
|
||||
r += str(s) + 's '
|
||||
elif not self.sec: # 0s
|
||||
r = '0 '
|
||||
return r[:-1]
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
@@ -0,0 +1,535 @@
|
||||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# Author: Serg G. Brester (sebres)
|
||||
#
|
||||
# This module was written as part of ban time increment feature.
|
||||
|
||||
__author__ = "Serg G. Brester (sebres)"
|
||||
__copyright__ = "Copyright (c) 2014 Serg G. Brester"
|
||||
__license__ = "GPL"
|
||||
|
||||
import threading
|
||||
from .jailthread import JailThread
|
||||
from .failmanager import FailManagerEmpty
|
||||
import os, logging, time, datetime, math, json, random
|
||||
import sys
|
||||
from ..helpers import getLogger
|
||||
from .mytime import MyTime
|
||||
from .utils import Utils
|
||||
|
||||
# Gets the instance of the logger.
|
||||
logSys = getLogger(__name__)
|
||||
|
||||
class ObserverThread(JailThread):
|
||||
"""Handles observing a database, managing bad ips and ban increment.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
Attributes
|
||||
----------
|
||||
daemon
|
||||
ident
|
||||
name
|
||||
status
|
||||
active : bool
|
||||
Control the state of the thread.
|
||||
idle : bool
|
||||
Control the idle state of the thread.
|
||||
sleeptime : int
|
||||
The time the thread sleeps for in the loop.
|
||||
"""
|
||||
|
||||
# observer is event driven and it sleep organized incremental, so sleep intervals can be shortly:
|
||||
DEFAULT_SLEEP_INTERVAL = Utils.DEFAULT_SLEEP_INTERVAL / 10
|
||||
|
||||
def __init__(self):
|
||||
# init thread
|
||||
super(ObserverThread, self).__init__(name='f2b/observer')
|
||||
# before started - idle:
|
||||
self.idle = True
|
||||
## Event queue
|
||||
self._queue_lock = threading.RLock()
|
||||
self._queue = []
|
||||
## Event, be notified if anything added to event queue
|
||||
self._notify = threading.Event()
|
||||
## Sleep for max 60 seconds, it possible to specify infinite to always sleep up to notifying via event,
|
||||
## but so we can later do some service "events" occurred infrequently directly in main loop of observer (not using queue)
|
||||
self.sleeptime = 60
|
||||
#
|
||||
self._timers = {}
|
||||
self._paused = False
|
||||
self.__db = None
|
||||
self.__db_purge_interval = 60*60
|
||||
# observer is a not main thread:
|
||||
self.daemon = True
|
||||
|
||||
def __getitem__(self, i):
|
||||
try:
|
||||
return self._queue[i]
|
||||
except KeyError:
|
||||
raise KeyError("Invalid event index : %s" % i)
|
||||
|
||||
def __delitem__(self, i):
|
||||
try:
|
||||
del self._queue[i]
|
||||
except KeyError:
|
||||
raise KeyError("Invalid event index: %s" % i)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._queue)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._queue)
|
||||
|
||||
def __eq__(self, other): # Required for Threading
|
||||
return False
|
||||
|
||||
def __hash__(self): # Required for Threading
|
||||
return id(self)
|
||||
|
||||
def add_named_timer(self, name, starttime, *event):
|
||||
"""Add a named timer event to queue will start (and wake) in 'starttime' seconds
|
||||
|
||||
Previous timer event with same name will be canceled and trigger self into
|
||||
queue after new 'starttime' value
|
||||
"""
|
||||
t = self._timers.get(name, None)
|
||||
if t is not None:
|
||||
t.cancel()
|
||||
t = threading.Timer(starttime, self.add, event)
|
||||
self._timers[name] = t
|
||||
t.start()
|
||||
|
||||
def add_timer(self, starttime, *event):
|
||||
"""Add a timer event to queue will start (and wake) in 'starttime' seconds
|
||||
"""
|
||||
# in testing we should wait (looping) for the possible time drifts:
|
||||
if MyTime.myTime is not None and starttime:
|
||||
# test time after short sleep:
|
||||
t = threading.Timer(Utils.DEFAULT_SLEEP_INTERVAL, self._delayedEvent,
|
||||
(MyTime.time() + starttime, time.time() + starttime, event)
|
||||
)
|
||||
t.start()
|
||||
return
|
||||
# add timer event:
|
||||
t = threading.Timer(starttime, self.add, event)
|
||||
t.start()
|
||||
|
||||
def _delayedEvent(self, endMyTime, endTime, event):
|
||||
if MyTime.time() >= endMyTime or time.time() >= endTime:
|
||||
self.add_timer(0, *event)
|
||||
return
|
||||
# repeat after short sleep:
|
||||
t = threading.Timer(Utils.DEFAULT_SLEEP_INTERVAL, self._delayedEvent,
|
||||
(endMyTime, endTime, event)
|
||||
)
|
||||
t.start()
|
||||
|
||||
def pulse_notify(self):
|
||||
"""Notify wakeup (sets /and resets/ notify event)
|
||||
"""
|
||||
if not self._paused:
|
||||
n = self._notify
|
||||
if n:
|
||||
n.set()
|
||||
#n.clear()
|
||||
|
||||
def add(self, *event):
|
||||
"""Add a event to queue and notify thread to wake up.
|
||||
"""
|
||||
## lock and add new event to queue:
|
||||
with self._queue_lock:
|
||||
self._queue.append(event)
|
||||
self.pulse_notify()
|
||||
|
||||
def add_wn(self, *event):
|
||||
"""Add a event to queue withouth notifying thread to wake up.
|
||||
"""
|
||||
## lock and add new event to queue:
|
||||
with self._queue_lock:
|
||||
self._queue.append(event)
|
||||
|
||||
def call_lambda(self, l, *args):
|
||||
l(*args)
|
||||
|
||||
def run(self):
|
||||
"""Main loop for Threading.
|
||||
|
||||
This function is the main loop of the thread.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when the thread exits nicely.
|
||||
"""
|
||||
logSys.info("Observer start...")
|
||||
## first time create named timer to purge database each hour (clean old entries) ...
|
||||
self.add_named_timer('DB_PURGE', self.__db_purge_interval, 'db_purge')
|
||||
## Mapping of all possible event types of observer:
|
||||
__meth = {
|
||||
# universal lambda:
|
||||
'call': self.call_lambda,
|
||||
# system and service events:
|
||||
'db_set': self.db_set,
|
||||
'db_purge': self.db_purge,
|
||||
# service events of observer self:
|
||||
'is_alive' : self.isAlive,
|
||||
'is_active': self.isActive,
|
||||
'start': self.start,
|
||||
'stop': self.stop,
|
||||
'nop': lambda:(),
|
||||
'shutdown': lambda:()
|
||||
}
|
||||
try:
|
||||
## check it self with sending is_alive event
|
||||
self.add('is_alive')
|
||||
## if we should stop - break a main loop
|
||||
while self.active:
|
||||
self.idle = False
|
||||
## check events available and execute all events from queue
|
||||
while not self._paused:
|
||||
## lock, check and pop one from begin of queue:
|
||||
try:
|
||||
ev = None
|
||||
with self._queue_lock:
|
||||
if len(self._queue):
|
||||
ev = self._queue.pop(0)
|
||||
if ev is None:
|
||||
break
|
||||
## retrieve method by name
|
||||
meth = ev[0]
|
||||
if not callable(ev[0]): meth = __meth.get(meth) or getattr(self, meth)
|
||||
## execute it with rest of event as variable arguments
|
||||
meth(*ev[1:])
|
||||
except Exception as e:
|
||||
#logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
logSys.error('%s', e, exc_info=True)
|
||||
## going sleep, wait for events (in queue)
|
||||
n = self._notify
|
||||
if n:
|
||||
self.idle = True
|
||||
n.wait(self.sleeptime)
|
||||
## wake up - reset signal now (we don't need it so long as we reed from queue)
|
||||
n.clear()
|
||||
if self._paused:
|
||||
continue
|
||||
else:
|
||||
## notify event deleted (shutdown) - just sleep a little bit (waiting for shutdown events, prevent high cpu usage)
|
||||
time.sleep(ObserverThread.DEFAULT_SLEEP_INTERVAL)
|
||||
## stop by shutdown and empty queue :
|
||||
if not self.is_full:
|
||||
break
|
||||
## end of main loop - exit
|
||||
logSys.info("Observer stopped, %s events remaining.", len(self._queue))
|
||||
self._notify = None
|
||||
#print("Observer stopped, %s events remaining." % len(self._queue))
|
||||
except Exception as e:
|
||||
logSys.error('Observer stopped after error: %s', e, exc_info=True)
|
||||
#print("Observer stopped with error: %s" % str(e))
|
||||
# clear all events - exit, for possible calls of wait_empty:
|
||||
with self._queue_lock:
|
||||
self._queue = []
|
||||
self.idle = True
|
||||
return True
|
||||
|
||||
def isAlive(self):
|
||||
#logSys.debug("Observer alive...")
|
||||
return True
|
||||
|
||||
def isActive(self, fromStr=None):
|
||||
# logSys.info("Observer alive, %s%s",
|
||||
# 'active' if self.active else 'inactive',
|
||||
# '' if fromStr is None else (", called from '%s'" % fromStr))
|
||||
return self.active
|
||||
|
||||
def start(self):
|
||||
with self._queue_lock:
|
||||
if not self.active:
|
||||
super(ObserverThread, self).start()
|
||||
|
||||
def stop(self, wtime=5, forceQuit=True):
|
||||
if self.active and self._notify:
|
||||
logSys.info("Observer stop ... try to end queue %s seconds", wtime)
|
||||
#print("Observer stop ....")
|
||||
# just add shutdown job to make possible wait later until full (events remaining)
|
||||
with self._queue_lock:
|
||||
self.add_wn('shutdown')
|
||||
#don't pulse - just set, because we will delete it hereafter (sometimes not wakeup)
|
||||
n = self._notify
|
||||
self._notify.set()
|
||||
#self.pulse_notify()
|
||||
self._notify = None
|
||||
# wait max wtime seconds until full (events remaining)
|
||||
if self.wait_empty(wtime) or forceQuit:
|
||||
n.clear()
|
||||
self.active = False; # leave outer (active) loop
|
||||
self._paused = True; # leave inner (queue) loop
|
||||
self.__db = None
|
||||
else:
|
||||
self._notify = n
|
||||
return self.wait_idle(min(wtime, 0.5)) and not self.is_full
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_full(self):
|
||||
with self._queue_lock:
|
||||
return True if len(self._queue) else False
|
||||
|
||||
def wait_empty(self, sleeptime=None):
|
||||
"""Wait observer is running and returns if observer has no more events (queue is empty)
|
||||
"""
|
||||
time.sleep(ObserverThread.DEFAULT_SLEEP_INTERVAL)
|
||||
if sleeptime is not None:
|
||||
e = MyTime.time() + sleeptime
|
||||
# block queue with not operation to be sure all really jobs are executed if nop goes from queue :
|
||||
if self._notify is not None:
|
||||
self.add_wn('nop')
|
||||
if self.is_full and self.idle:
|
||||
self.pulse_notify()
|
||||
while self.is_full:
|
||||
if sleeptime is not None and MyTime.time() > e:
|
||||
break
|
||||
time.sleep(ObserverThread.DEFAULT_SLEEP_INTERVAL)
|
||||
# wait idle to be sure the last queue element is processed (because pop event before processing it) :
|
||||
self.wait_idle(0.001)
|
||||
return not self.is_full
|
||||
|
||||
|
||||
def wait_idle(self, sleeptime=None):
|
||||
"""Wait observer is running and returns if observer idle (observer sleeps)
|
||||
"""
|
||||
time.sleep(ObserverThread.DEFAULT_SLEEP_INTERVAL)
|
||||
if self.idle:
|
||||
return True
|
||||
if sleeptime is not None:
|
||||
e = MyTime.time() + sleeptime
|
||||
while not self.idle:
|
||||
if sleeptime is not None and MyTime.time() > e:
|
||||
break
|
||||
time.sleep(ObserverThread.DEFAULT_SLEEP_INTERVAL)
|
||||
return self.idle
|
||||
|
||||
@property
|
||||
def paused(self):
|
||||
return self._paused;
|
||||
|
||||
@paused.setter
|
||||
def paused(self, pause):
|
||||
if self._paused == pause:
|
||||
return
|
||||
self._paused = pause
|
||||
# wake after pause ended
|
||||
self.pulse_notify()
|
||||
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Status of observer to be implemented. [TODO]
|
||||
"""
|
||||
return ('', '')
|
||||
|
||||
## -----------------------------------------
|
||||
## [Async] database service functionality ...
|
||||
## -----------------------------------------
|
||||
|
||||
def db_set(self, db):
|
||||
self.__db = db
|
||||
|
||||
def db_purge(self):
|
||||
logSys.debug("Purge database event occurred")
|
||||
if self.__db is not None:
|
||||
self.__db.purge()
|
||||
# trigger timer again ...
|
||||
self.add_named_timer('DB_PURGE', self.__db_purge_interval, 'db_purge')
|
||||
|
||||
## -----------------------------------------
|
||||
## [Async] ban time increment functionality ...
|
||||
## -----------------------------------------
|
||||
|
||||
def failureFound(self, failManager, jail, ticket):
|
||||
""" Notify observer a failure for ip was found
|
||||
|
||||
Observer will check ip was known (bad) and possibly increase an retry count
|
||||
"""
|
||||
# check jail active :
|
||||
if not jail.isAlive() or not jail.getBanTimeExtra("increment"):
|
||||
return
|
||||
ip = ticket.getIP()
|
||||
unixTime = ticket.getTime()
|
||||
logSys.debug("[%s] Observer: failure found %s", jail.name, ip)
|
||||
# increase retry count for known (bad) ip, corresponding banCount of it (one try will count than 2, 3, 5, 9 ...) :
|
||||
banCount = 0
|
||||
retryCount = 1
|
||||
timeOfBan = None
|
||||
try:
|
||||
maxRetry = failManager.getMaxRetry()
|
||||
db = jail.database
|
||||
if db is not None:
|
||||
for banCount, timeOfBan, lastBanTime in db.getBan(ip, jail):
|
||||
banCount = max(banCount, ticket.getBanCount())
|
||||
retryCount = ((1 << (banCount if banCount < 20 else 20))/2 + 1)
|
||||
# if lastBanTime == -1 or timeOfBan + lastBanTime * 2 > MyTime.time():
|
||||
# retryCount = maxRetry
|
||||
break
|
||||
retryCount = min(retryCount, maxRetry)
|
||||
# check this ticket already known (line was already processed and in the database and will be restored from there):
|
||||
if timeOfBan is not None and unixTime <= timeOfBan:
|
||||
logSys.debug("[%s] Ignore failure %s before last ban %s < %s, restored",
|
||||
jail.name, ip, unixTime, timeOfBan)
|
||||
return
|
||||
# for not increased failures observer should not add it to fail manager, because was already added by filter self
|
||||
if retryCount <= 1:
|
||||
return
|
||||
# retry counter was increased - add it again:
|
||||
logSys.info("[%s] Found %s, bad - %s, %s # -> %s%s", jail.name, ip,
|
||||
MyTime.time2str(unixTime), banCount, retryCount,
|
||||
(', Ban' if retryCount >= maxRetry else ''))
|
||||
# retryCount-1, because a ticket was already once incremented by filter self
|
||||
retryCount = failManager.addFailure(ticket, retryCount - 1, True)
|
||||
ticket.setBanCount(banCount)
|
||||
# after observe we have increased attempt count, compare it >= maxretry ...
|
||||
if retryCount >= maxRetry:
|
||||
# perform the banning of the IP now (again)
|
||||
# [todo]: this code part will be used multiple times - optimize it later.
|
||||
try: # pragma: no branch - exception is the only way out
|
||||
while True:
|
||||
ticket = failManager.toBan(ip)
|
||||
jail.putFailTicket(ticket)
|
||||
except FailManagerEmpty:
|
||||
failManager.cleanup(MyTime.time())
|
||||
|
||||
except Exception as e:
|
||||
logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
|
||||
|
||||
class BanTimeIncr:
|
||||
def __init__(self, banTime, banCount):
|
||||
self.Time = banTime
|
||||
self.Count = banCount
|
||||
|
||||
def calcBanTime(self, jail, banTime, banCount):
|
||||
be = jail.getBanTimeExtra()
|
||||
return be['evformula'](self.BanTimeIncr(banTime, banCount))
|
||||
|
||||
def incrBanTime(self, jail, banTime, ticket):
|
||||
"""Check for IP address to increment ban time (if was already banned).
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
new ban time.
|
||||
"""
|
||||
# check jail active :
|
||||
if not jail.isAlive() or not jail.database:
|
||||
return banTime
|
||||
be = jail.getBanTimeExtra()
|
||||
ip = ticket.getIP()
|
||||
orgBanTime = banTime
|
||||
# check ip was already banned (increment time of ban):
|
||||
try:
|
||||
if banTime > 0 and be.get('increment', False):
|
||||
# search IP in database and increase time if found:
|
||||
for banCount, timeOfBan, lastBanTime in \
|
||||
jail.database.getBan(ip, jail, overalljails=be.get('overalljails', False)) \
|
||||
:
|
||||
# increment count in ticket (if still not increased from banmanager, test-cases?):
|
||||
if banCount >= ticket.getBanCount():
|
||||
ticket.setBanCount(banCount+1)
|
||||
logSys.debug('IP %s was already banned: %s #, %s', ip, banCount, timeOfBan);
|
||||
# calculate new ban time
|
||||
if banCount > 0:
|
||||
banTime = be['evformula'](self.BanTimeIncr(banTime, banCount))
|
||||
ticket.setBanTime(banTime)
|
||||
# check current ticket time to prevent increasing for twice read tickets (restored from log file besides database after restart)
|
||||
if ticket.getTime() > timeOfBan:
|
||||
logSys.info('[%s] IP %s is bad: %s # last %s - incr %s to %s' % (jail.name, ip, banCount,
|
||||
MyTime.time2str(timeOfBan),
|
||||
MyTime.seconds2str(orgBanTime), MyTime.seconds2str(banTime)))
|
||||
else:
|
||||
ticket.restored = True
|
||||
break
|
||||
except Exception as e:
|
||||
logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
return banTime
|
||||
|
||||
def banFound(self, ticket, jail, btime):
|
||||
""" Notify observer a ban occured for ip
|
||||
|
||||
Observer will check ip was known (bad) and possibly increase/prolong a ban time
|
||||
Secondary we will actualize the bans and bips (bad ip) in database
|
||||
"""
|
||||
if ticket.restored: # pragma: no cover (normally not resored tickets only)
|
||||
return
|
||||
try:
|
||||
oldbtime = btime
|
||||
ip = ticket.getIP()
|
||||
logSys.debug("[%s] Observer: ban found %s, %s", jail.name, ip, btime)
|
||||
# if not permanent and ban time was not set - check time should be increased:
|
||||
if btime != -1 and ticket.getBanTime() is None:
|
||||
btime = self.incrBanTime(jail, btime, ticket)
|
||||
# if we should prolong ban time:
|
||||
if btime == -1 or btime > oldbtime:
|
||||
ticket.setBanTime(btime)
|
||||
# if not permanent
|
||||
if btime != -1:
|
||||
bendtime = ticket.getTime() + btime
|
||||
logtime = (MyTime.seconds2str(btime), MyTime.time2str(bendtime))
|
||||
# check ban is not too old :
|
||||
if bendtime < MyTime.time():
|
||||
logSys.debug('Ignore old bantime %s', logtime[1])
|
||||
return False
|
||||
else:
|
||||
logtime = ('permanent', 'infinite')
|
||||
# if ban time was prolonged - log again with new ban time:
|
||||
if btime != oldbtime:
|
||||
logSys.notice("[%s] Increase Ban %s (%d # %s -> %s)", jail.name,
|
||||
ip, ticket.getBanCount(), *logtime)
|
||||
# delayed prolonging ticket via actions that expected this (not later than 10 sec):
|
||||
logSys.log(5, "[%s] Observer: prolong %s in %s", jail.name, ip, (btime, oldbtime))
|
||||
self.add_timer(min(10, max(0, btime - oldbtime - 5)), self.prolongBan, ticket, jail)
|
||||
# add ticket to database, but only if was not restored (not already read from database):
|
||||
if jail.database is not None and not ticket.restored:
|
||||
# add to database always only after ban time was calculated an not yet already banned:
|
||||
jail.database.addBan(jail, ticket)
|
||||
except Exception as e:
|
||||
logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
|
||||
def prolongBan(self, ticket, jail):
|
||||
""" Notify observer a ban occured for ip
|
||||
|
||||
Observer will check ip was known (bad) and possibly increase/prolong a ban time
|
||||
Secondary we will actualize the bans and bips (bad ip) in database
|
||||
"""
|
||||
try:
|
||||
btime = ticket.getBanTime()
|
||||
ip = ticket.getIP()
|
||||
logSys.debug("[%s] Observer: prolong %s, %s", jail.name, ip, btime)
|
||||
# prolong ticket via actions that expected this:
|
||||
jail.actions._prolongBan(ticket)
|
||||
except Exception as e:
|
||||
logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
|
||||
|
||||
# Global observer initial created in server (could be later rewriten via singleton)
|
||||
class _Observers:
|
||||
def __init__(self):
|
||||
self.Main = None
|
||||
|
||||
Observers = _Observers()
|
||||
@@ -32,8 +32,9 @@ import signal
|
||||
import stat
|
||||
import sys
|
||||
|
||||
from .observer import Observers, ObserverThread
|
||||
from .jails import Jails
|
||||
from .filter import FileFilter, JournalFilter
|
||||
from .filter import DNSUtils, FileFilter, JournalFilter
|
||||
from .transmitter import Transmitter
|
||||
from .asyncserver import AsyncServer, AsyncServerException
|
||||
from .. import version
|
||||
@@ -111,7 +112,7 @@ class Server:
|
||||
self.__prev_signals[s] = signal.getsignal(s)
|
||||
signal.signal(s, new)
|
||||
|
||||
def start(self, sock, pidfile, force=False, conf={}):
|
||||
def start(self, sock, pidfile, force=False, observer=True, conf={}):
|
||||
# First set the mask to only allow access to owner
|
||||
os.umask(0o077)
|
||||
# Second daemonize before logging etc, because it will close all handles:
|
||||
@@ -165,6 +166,12 @@ class Server:
|
||||
except (OSError, IOError) as e: # pragma: no cover
|
||||
logSys.error("Unable to create PID file: %s", e)
|
||||
|
||||
# Create observers and start it:
|
||||
if observer:
|
||||
if Observers.Main is None:
|
||||
Observers.Main = ObserverThread()
|
||||
Observers.Main.start()
|
||||
|
||||
# Start the communication
|
||||
logSys.debug("Starting communication")
|
||||
try:
|
||||
@@ -205,21 +212,33 @@ class Server:
|
||||
for s, sh in self.__prev_signals.iteritems():
|
||||
signal.signal(s, sh)
|
||||
|
||||
# Give observer a small chance to complete its work before exit
|
||||
obsMain = Observers.Main
|
||||
if obsMain is not None:
|
||||
if obsMain.stop(forceQuit=False):
|
||||
obsMain = None
|
||||
Observers.Main = None
|
||||
|
||||
# Now stop all the jails
|
||||
self.stopAllJail()
|
||||
|
||||
# Stop observer ultimately
|
||||
if obsMain is not None:
|
||||
obsMain.stop()
|
||||
|
||||
# Explicit close database (server can leave in a thread,
|
||||
# so delayed GC can prevent commiting changes)
|
||||
if self.__db:
|
||||
self.__db.close()
|
||||
self.__db = None
|
||||
|
||||
# Stop async
|
||||
# Stop async and exit
|
||||
if self.__asyncServer is not None:
|
||||
self.__asyncServer.stop()
|
||||
self.__asyncServer = None
|
||||
logSys.info("Exiting Fail2ban")
|
||||
|
||||
|
||||
def addJail(self, name, backend):
|
||||
addflg = True
|
||||
if self.__reload_state.get(name) and self.__jails.exists(name):
|
||||
@@ -274,6 +293,11 @@ class Server:
|
||||
for name in self.__jails.keys():
|
||||
self.delJail(name, stop=False, join=True)
|
||||
|
||||
def clearCaches(self):
|
||||
# we need to clear caches, to be able to recognize new IPs/families etc:
|
||||
DNSUtils.CACHE_nameToIp.clear()
|
||||
DNSUtils.CACHE_ipToName.clear()
|
||||
|
||||
def reloadJails(self, name, opts, begin):
|
||||
if begin:
|
||||
# begin reload:
|
||||
@@ -295,6 +319,8 @@ class Server:
|
||||
if "--restart" in opts:
|
||||
self.stopJail(name)
|
||||
else:
|
||||
# invalidate caches by reload
|
||||
self.clearCaches()
|
||||
# first unban all ips (will be not restored after (re)start):
|
||||
if "--unban" in opts:
|
||||
self.setUnbanIP()
|
||||
@@ -549,6 +575,27 @@ class Server:
|
||||
|
||||
def getBanTime(self, name):
|
||||
return self.__jails[name].actions.getBanTime()
|
||||
|
||||
def getBanList(self, name, withTime=False):
|
||||
"""Returns the list of banned IP addresses for a jail.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The name of a jail.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
The list of banned IP addresses.
|
||||
"""
|
||||
return self.__jails[name].actions.getBanList(withTime)
|
||||
|
||||
def setBanTimeExtra(self, name, opt, value):
|
||||
self.__jails[name].setBanTimeExtra(opt, value)
|
||||
|
||||
def getBanTimeExtra(self, name, opt):
|
||||
return self.__jails[name].getBanTimeExtra(opt)
|
||||
|
||||
def isStarted(self):
|
||||
return self.__asyncServer is not None and self.__asyncServer.isActive()
|
||||
@@ -763,6 +810,11 @@ class Server:
|
||||
logSys.info("flush performed on %s" % self.__logTarget)
|
||||
return "flushed"
|
||||
|
||||
@staticmethod
|
||||
def setIPv6IsAllowed(value):
|
||||
value = _as_bool(value) if value != 'auto' else None
|
||||
return DNSUtils.setIPv6IsAllowed(value)
|
||||
|
||||
def setThreadOptions(self, value):
|
||||
for o, v in value.iteritems():
|
||||
if o == 'stacksize':
|
||||
@@ -793,6 +845,8 @@ class Server:
|
||||
logSys.error(
|
||||
"Unable to import fail2ban database module as sqlite "
|
||||
"is not available.")
|
||||
if Observers.Main is not None:
|
||||
Observers.Main.db_set(self.__db)
|
||||
|
||||
def getDatabase(self):
|
||||
return self.__db
|
||||
|
||||
+61
-28
@@ -30,17 +30,6 @@ locale_time = LocaleTime()
|
||||
TZ_ABBR_RE = r"[A-Z](?:[A-Z]{2,4})?"
|
||||
FIXED_OFFSET_TZ_RE = re.compile(r"(%s)?([+-][01]\d(?::?\d{2})?)?$" % (TZ_ABBR_RE,))
|
||||
|
||||
def _getYearCentRE(cent=(0,3), distance=3, now=(MyTime.now(), MyTime.alternateNow)):
|
||||
""" Build century regex for last year and the next years (distance).
|
||||
|
||||
Thereby respect possible run in the test-cases (alternate date used there)
|
||||
"""
|
||||
cent = lambda year, f=cent[0], t=cent[1]: str(year)[f:t]
|
||||
exprset = set( cent(now[0].year + i) for i in (-1, distance) )
|
||||
if len(now) and now[1]:
|
||||
exprset |= set( cent(now[1].year + i) for i in (-1, distance) )
|
||||
return "(?:%s)" % "|".join(exprset) if len(exprset) > 1 else "".join(exprset)
|
||||
|
||||
timeRE = TimeRE()
|
||||
|
||||
# %k - one- or two-digit number giving the hour of the day (0-23) on a 24-hour clock,
|
||||
@@ -63,20 +52,68 @@ timeRE['z'] = r"(?P<z>Z|UTC|GMT|[+-][01]\d(?::?\d{2})?)"
|
||||
timeRE['ExZ'] = r"(?P<Z>%s)" % (TZ_ABBR_RE,)
|
||||
timeRE['Exz'] = r"(?P<z>(?:%s)?[+-][01]\d(?::?\d{2})?|%s)" % (TZ_ABBR_RE, TZ_ABBR_RE)
|
||||
|
||||
# overwrite default patterns, since they can be non-optimal:
|
||||
timeRE['d'] = r"(?P<d>[1-2]\d|[0 ]?[1-9]|3[0-1])"
|
||||
timeRE['m'] = r"(?P<m>0?[1-9]|1[0-2])"
|
||||
timeRE['Y'] = r"(?P<Y>\d{4})"
|
||||
timeRE['H'] = r"(?P<H>[0-1]?\d|2[0-3])"
|
||||
timeRE['M'] = r"(?P<M>[0-5]?\d)"
|
||||
timeRE['S'] = r"(?P<S>[0-5]?\d|6[0-1])"
|
||||
|
||||
# Extend build-in TimeRE with some exact patterns
|
||||
# exact two-digit patterns:
|
||||
timeRE['Exd'] = r"(?P<d>3[0-1]|[1-2]\d|0[1-9])"
|
||||
timeRE['Exm'] = r"(?P<m>1[0-2]|0[1-9])"
|
||||
timeRE['ExH'] = r"(?P<H>2[0-3]|[0-1]\d)"
|
||||
timeRE['Exk'] = r" ?(?P<H>2[0-3]|[0-1]\d|\d)"
|
||||
timeRE['Exd'] = r"(?P<d>[1-2]\d|0[1-9]|3[0-1])"
|
||||
timeRE['Exm'] = r"(?P<m>0[1-9]|1[0-2])"
|
||||
timeRE['ExH'] = r"(?P<H>[0-1]\d|2[0-3])"
|
||||
timeRE['Exk'] = r" ?(?P<H>[0-1]?\d|2[0-3])"
|
||||
timeRE['Exl'] = r" ?(?P<I>1[0-2]|\d)"
|
||||
timeRE['ExM'] = r"(?P<M>[0-5]\d)"
|
||||
timeRE['ExS'] = r"(?P<S>6[0-1]|[0-5]\d)"
|
||||
# more precise year patterns, within same century of last year and
|
||||
# the next 3 years (for possible long uptime of fail2ban); thereby
|
||||
# respect possible run in the test-cases (alternate date used there):
|
||||
timeRE['ExY'] = r"(?P<Y>%s\d)" % _getYearCentRE(cent=(0,3), distance=3)
|
||||
timeRE['Exy'] = r"(?P<y>%s\d)" % _getYearCentRE(cent=(2,3), distance=3)
|
||||
timeRE['ExS'] = r"(?P<S>[0-5]\d|6[0-1])"
|
||||
|
||||
def _updateTimeRE():
|
||||
def _getYearCentRE(cent=(0,3), distance=3, now=(MyTime.now(), MyTime.alternateNow)):
|
||||
""" Build century regex for last year and the next years (distance).
|
||||
|
||||
Thereby respect possible run in the test-cases (alternate date used there)
|
||||
"""
|
||||
cent = lambda year, f=cent[0], t=cent[1]: str(year)[f:t]
|
||||
def grp(exprset):
|
||||
c = None
|
||||
if len(exprset) > 1:
|
||||
for i in exprset:
|
||||
if c is None or i[0:-1] == c:
|
||||
c = i[0:-1]
|
||||
else:
|
||||
c = None
|
||||
break
|
||||
if not c:
|
||||
for i in exprset:
|
||||
if c is None or i[0] == c:
|
||||
c = i[0]
|
||||
else:
|
||||
c = None
|
||||
break
|
||||
if c:
|
||||
return "%s%s" % (c, grp([i[len(c):] for i in exprset]))
|
||||
return ("(?:%s)" % "|".join(exprset) if len(exprset[0]) > 1 else "[%s]" % "".join(exprset)) \
|
||||
if len(exprset) > 1 else "".join(exprset)
|
||||
exprset = set( cent(now[0].year + i) for i in (-1, distance) )
|
||||
if len(now) > 1 and now[1]:
|
||||
exprset |= set( cent(now[1].year + i) for i in xrange(-1, now[0].year-now[1].year+1, distance) )
|
||||
return grp(sorted(list(exprset)))
|
||||
|
||||
# more precise year patterns, within same century of last year and
|
||||
# the next 3 years (for possible long uptime of fail2ban); thereby
|
||||
# consider possible run in the test-cases (alternate date used there),
|
||||
# so accept years: 20xx (from test-date or 2001 up to current century)
|
||||
timeRE['ExY'] = r"(?P<Y>%s\d)" % _getYearCentRE(cent=(0,3), distance=3,
|
||||
now=(datetime.datetime.now(), datetime.datetime.fromtimestamp(
|
||||
min(MyTime.alternateNowTime or 978393600, 978393600))
|
||||
)
|
||||
)
|
||||
timeRE['Exy'] = r"(?P<y>\d{2})"
|
||||
|
||||
_updateTimeRE()
|
||||
|
||||
def getTimePatternRE():
|
||||
keys = timeRE.keys()
|
||||
@@ -168,9 +205,9 @@ def reGroupDictStrptime(found_dict, msec=False, default_tz=None):
|
||||
"""
|
||||
|
||||
now = \
|
||||
year = month = day = hour = minute = tzoffset = \
|
||||
year = month = day = tzoffset = \
|
||||
weekday = julian = week_of_year = None
|
||||
second = fraction = 0
|
||||
hour = minute = second = fraction = 0
|
||||
for key, val in found_dict.iteritems():
|
||||
if val is None: continue
|
||||
# Directives not explicitly handled below:
|
||||
@@ -234,16 +271,12 @@ def reGroupDictStrptime(found_dict, msec=False, default_tz=None):
|
||||
week_of_year = int(val)
|
||||
# U starts week on Sunday, W - on Monday
|
||||
week_of_year_start = 6 if key == 'U' else 0
|
||||
elif key == 'z':
|
||||
elif key in ('z', 'Z'):
|
||||
z = val
|
||||
if z in ("Z", "UTC", "GMT"):
|
||||
tzoffset = 0
|
||||
else:
|
||||
tzoffset = zone2offset(z, 0); # currently offset-based only
|
||||
elif key == 'Z':
|
||||
z = val
|
||||
if z in ("UTC", "GMT"):
|
||||
tzoffset = 0
|
||||
|
||||
# Fail2Ban will assume it's this year
|
||||
assume_year = False
|
||||
|
||||
+35
-17
@@ -24,8 +24,6 @@ __author__ = "Cyril Jaquier"
|
||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||
__license__ = "GPL"
|
||||
|
||||
import sys
|
||||
|
||||
from ..helpers import getLogger
|
||||
from .ipdns import IPAddr
|
||||
from .mytime import MyTime
|
||||
@@ -35,6 +33,7 @@ logSys = getLogger(__name__)
|
||||
|
||||
|
||||
class Ticket(object):
|
||||
__slots__ = ('_ip', '_flags', '_banCount', '_banTime', '_time', '_data', '_retry', '_lastReset')
|
||||
|
||||
MAX_TIME = 0X7FFFFFFFFFFF ;# 4461763-th year
|
||||
|
||||
@@ -61,35 +60,44 @@ class Ticket(object):
|
||||
self._data[k] = v
|
||||
if ticket:
|
||||
# ticket available - copy whole information from ticket:
|
||||
self.__dict__.update(i for i in ticket.__dict__.iteritems() if i[0] in self.__dict__)
|
||||
self.update(ticket)
|
||||
#self.__dict__.update(i for i in ticket.__dict__.iteritems() if i[0] in self.__dict__)
|
||||
|
||||
def __str__(self):
|
||||
return "%s: ip=%s time=%s #attempts=%d matches=%r" % \
|
||||
(self.__class__.__name__.split('.')[-1], self.__ip, self._time,
|
||||
self._data['failures'], self._data.get('matches', []))
|
||||
return "%s: ip=%s time=%s bantime=%s bancount=%s #attempts=%d matches=%r" % \
|
||||
(self.__class__.__name__.split('.')[-1], self._ip, self._time,
|
||||
self._banTime, self._banCount,
|
||||
self._data['failures'], self._data.get('matches', []))
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
try:
|
||||
return self.__ip == other.__ip and \
|
||||
return self._ip == other._ip and \
|
||||
round(self._time, 2) == round(other._time, 2) and \
|
||||
self._data == other._data
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
def update(self, ticket):
|
||||
for n in ticket.__slots__:
|
||||
v = getattr(ticket, n, None)
|
||||
if v is not None:
|
||||
setattr(self, n, v)
|
||||
|
||||
|
||||
def setIP(self, value):
|
||||
# guarantee using IPAddr instead of unicode, str for the IP
|
||||
if isinstance(value, basestring):
|
||||
value = IPAddr(value)
|
||||
self.__ip = value
|
||||
self._ip = value
|
||||
|
||||
def getID(self):
|
||||
return self._data.get('fid', self.__ip)
|
||||
return self._data.get('fid', self._ip)
|
||||
|
||||
def getIP(self):
|
||||
return self.__ip
|
||||
return self._ip
|
||||
|
||||
def setTime(self, value):
|
||||
self._time = value
|
||||
@@ -98,16 +106,17 @@ class Ticket(object):
|
||||
return self._time
|
||||
|
||||
def setBanTime(self, value):
|
||||
self._banTime = value;
|
||||
self._banTime = value
|
||||
|
||||
def getBanTime(self, defaultBT=None):
|
||||
return (self._banTime if self._banTime is not None else defaultBT)
|
||||
|
||||
def setBanCount(self, value):
|
||||
self._banCount = value;
|
||||
def setBanCount(self, value, always=False):
|
||||
if always or value > self._banCount:
|
||||
self._banCount = value
|
||||
|
||||
def incrBanCount(self, value = 1):
|
||||
self._banCount += value;
|
||||
def incrBanCount(self, value=1):
|
||||
self._banCount += value
|
||||
|
||||
def getBanCount(self):
|
||||
return self._banCount;
|
||||
@@ -267,10 +276,19 @@ class FailTicket(Ticket):
|
||||
else:
|
||||
self._data['matches'] = matches
|
||||
|
||||
@staticmethod
|
||||
def wrap(o):
|
||||
o.__class__ = FailTicket
|
||||
return o
|
||||
|
||||
##
|
||||
# Ban Ticket.
|
||||
#
|
||||
# This class extends the Ticket class. It is mainly used by the BanManager.
|
||||
|
||||
class BanTicket(Ticket):
|
||||
pass
|
||||
class BanTicket(FailTicket):
|
||||
|
||||
@staticmethod
|
||||
def wrap(o):
|
||||
o.__class__ = BanTicket
|
||||
return o
|
||||
|
||||
@@ -173,6 +173,11 @@ class Transmitter:
|
||||
return self.__server.getSyslogSocket()
|
||||
else:
|
||||
raise Exception("Failed to change syslog socket")
|
||||
elif name == "allowipv6":
|
||||
value = command[1]
|
||||
self.__server.setIPv6IsAllowed(value)
|
||||
if self.__quiet: return
|
||||
return value
|
||||
#Thread
|
||||
elif name == "thread":
|
||||
value = command[1]
|
||||
@@ -348,6 +353,12 @@ class Transmitter:
|
||||
value = command[2:]
|
||||
if self.__quiet: return
|
||||
return self.__server.addAttemptIP(name, *value)
|
||||
elif command[1].startswith("bantime."):
|
||||
value = command[2]
|
||||
opt = command[1][len("bantime."):]
|
||||
self.__server.setBanTimeExtra(name, opt, value)
|
||||
if self.__quiet: return
|
||||
return self.__server.getBanTimeExtra(name, opt)
|
||||
elif command[1] == "banip":
|
||||
value = command[2:]
|
||||
return self.__server.setBanIP(name,value)
|
||||
@@ -470,6 +481,12 @@ class Transmitter:
|
||||
# Action
|
||||
elif command[1] == "bantime":
|
||||
return self.__server.getBanTime(name)
|
||||
elif command[1] == "banip":
|
||||
return self.__server.getBanList(name,
|
||||
withTime=len(command) > 2 and command[2] == "--with-time")
|
||||
elif command[1].startswith("bantime."):
|
||||
opt = command[1][len("bantime."):]
|
||||
return self.__server.getBanTimeExtra(name, opt)
|
||||
elif command[1] == "actions":
|
||||
return self.__server.getActions(name).keys()
|
||||
elif command[1] == "action":
|
||||
|
||||
@@ -332,11 +332,9 @@ class Utils():
|
||||
timeout_expr = lambda: time.time() > time0
|
||||
else:
|
||||
timeout_expr = timeout
|
||||
if not interval:
|
||||
interval = Utils.DEFAULT_SLEEP_INTERVAL
|
||||
if timeout_expr():
|
||||
break
|
||||
stm = min(stm + interval, Utils.DEFAULT_SLEEP_TIME)
|
||||
stm = min(stm + (interval or Utils.DEFAULT_SLEEP_INTERVAL), Utils.DEFAULT_SLEEP_TIME)
|
||||
time.sleep(stm)
|
||||
return ret
|
||||
|
||||
@@ -355,7 +353,7 @@ class Utils():
|
||||
return e.errno == errno.EPERM
|
||||
else:
|
||||
return True
|
||||
else: # pragma : no cover (no windows currently supported)
|
||||
else: # pragma: no cover (no windows currently supported)
|
||||
@staticmethod
|
||||
def pid_exists(pid):
|
||||
import ctypes
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
|
||||
# vi: set ft=python sts=4 ts=4 sw=4 noet :
|
||||
|
||||
# This file is part of Fail2Ban.
|
||||
#
|
||||
# Fail2Ban is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Fail2Ban is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Fail2Ban; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
import os
|
||||
import unittest
|
||||
import sys
|
||||
from functools import wraps
|
||||
from socket import timeout
|
||||
from ssl import SSLError
|
||||
|
||||
from ..actiontestcase import CallingMap
|
||||
from ..dummyjail import DummyJail
|
||||
from ..servertestcase import IPAddr
|
||||
from ..utils import LogCaptureTestCase, CONFIG_DIR
|
||||
|
||||
if sys.version_info >= (3, ): # pragma: 2.x no cover
|
||||
from urllib.error import HTTPError, URLError
|
||||
else: # pragma: 3.x no cover
|
||||
from urllib2 import HTTPError, URLError
|
||||
|
||||
def skip_if_not_available(f):
|
||||
"""Helper to decorate tests to skip in case of timeout/http-errors like "502 bad gateway".
|
||||
"""
|
||||
@wraps(f)
|
||||
def wrapper(self, *args):
|
||||
try:
|
||||
return f(self, *args)
|
||||
except (SSLError, HTTPError, URLError, timeout) as e: # pragma: no cover - timeout/availability issues
|
||||
if not isinstance(e, timeout) and 'timed out' not in str(e):
|
||||
if not hasattr(e, 'code') or e.code > 200 and e.code <= 404:
|
||||
raise
|
||||
raise unittest.SkipTest('Skip test because of %s' % e)
|
||||
return wrapper
|
||||
|
||||
if sys.version_info >= (2,7): # pragma: no cover - may be unavailable
|
||||
class BadIPsActionTest(LogCaptureTestCase):
|
||||
|
||||
available = True, None
|
||||
pythonModule = None
|
||||
modAction = None
|
||||
|
||||
@skip_if_not_available
|
||||
def setUp(self):
|
||||
"""Call before every test case."""
|
||||
super(BadIPsActionTest, self).setUp()
|
||||
unittest.F2B.SkipIfNoNetwork()
|
||||
|
||||
self.jail = DummyJail()
|
||||
|
||||
self.jail.actions.add("test")
|
||||
|
||||
pythonModuleName = os.path.join(CONFIG_DIR, "action.d", "badips.py")
|
||||
|
||||
# check availability (once if not alive, used shorter timeout as in test cases):
|
||||
if BadIPsActionTest.available[0]:
|
||||
if not BadIPsActionTest.modAction:
|
||||
if not BadIPsActionTest.pythonModule:
|
||||
BadIPsActionTest.pythonModule = self.jail.actions._load_python_module(pythonModuleName)
|
||||
BadIPsActionTest.modAction = BadIPsActionTest.pythonModule.Action
|
||||
self.jail.actions._load_python_module(pythonModuleName)
|
||||
BadIPsActionTest.available = BadIPsActionTest.modAction.isAvailable(timeout=2 if unittest.F2B.fast else 30)
|
||||
if not BadIPsActionTest.available[0]:
|
||||
raise unittest.SkipTest('Skip test because service is not available: %s' % BadIPsActionTest.available[1])
|
||||
|
||||
self.jail.actions.add("badips", pythonModuleName, initOpts={
|
||||
'category': "ssh",
|
||||
'banaction': "test",
|
||||
'age': "2w",
|
||||
'score': 5,
|
||||
#'key': "fail2ban-test-suite",
|
||||
#'bankey': "fail2ban-test-suite",
|
||||
'timeout': (3 if unittest.F2B.fast else 60),
|
||||
})
|
||||
self.action = self.jail.actions["badips"]
|
||||
|
||||
def tearDown(self):
|
||||
"""Call after every test case."""
|
||||
# Must cancel timer!
|
||||
if self.action._timer:
|
||||
self.action._timer.cancel()
|
||||
super(BadIPsActionTest, self).tearDown()
|
||||
|
||||
@skip_if_not_available
|
||||
def testCategory(self):
|
||||
categories = self.action.getCategories()
|
||||
self.assertIn("ssh", categories)
|
||||
self.assertTrue(len(categories) >= 10)
|
||||
|
||||
self.assertRaises(
|
||||
ValueError, setattr, self.action, "category",
|
||||
"invalid-category")
|
||||
|
||||
# Not valid for reporting category...
|
||||
self.assertRaises(
|
||||
ValueError, setattr, self.action, "category", "mail")
|
||||
# but valid for blacklisting.
|
||||
self.action.bancategory = "mail"
|
||||
|
||||
@skip_if_not_available
|
||||
def testScore(self):
|
||||
self.assertRaises(ValueError, setattr, self.action, "score", -5)
|
||||
self.action.score = 3
|
||||
self.action.score = "3"
|
||||
|
||||
@skip_if_not_available
|
||||
def testBanaction(self):
|
||||
self.assertRaises(
|
||||
ValueError, setattr, self.action, "banaction",
|
||||
"invalid-action")
|
||||
self.action.banaction = "test"
|
||||
|
||||
@skip_if_not_available
|
||||
def testUpdateperiod(self):
|
||||
self.assertRaises(
|
||||
ValueError, setattr, self.action, "updateperiod", -50)
|
||||
self.assertRaises(
|
||||
ValueError, setattr, self.action, "updateperiod", 0)
|
||||
self.action.updateperiod = 900
|
||||
self.action.updateperiod = "900"
|
||||
|
||||
@skip_if_not_available
|
||||
def testStartStop(self):
|
||||
self.action.start()
|
||||
self.assertTrue(len(self.action._bannedips) > 10,
|
||||
"%s is fewer as 10: %r" % (len(self.action._bannedips), self.action._bannedips))
|
||||
self.action.stop()
|
||||
self.assertTrue(len(self.action._bannedips) == 0)
|
||||
|
||||
@skip_if_not_available
|
||||
def testBanIP(self):
|
||||
aInfo = CallingMap({
|
||||
'ip': IPAddr('192.0.2.1')
|
||||
})
|
||||
self.action.ban(aInfo)
|
||||
self.assertLogged('badips.com: ban', wait=True)
|
||||
self.pruneLog()
|
||||
# produce an error using wrong category/IP:
|
||||
self.action._category = 'f2b-this-category-dont-available-test-suite-only'
|
||||
aInfo['ip'] = ''
|
||||
self.assertRaises(BadIPsActionTest.pythonModule.HTTPError, self.action.ban, aInfo)
|
||||
self.assertLogged('IP is invalid', 'invalid category', wait=True, all=False)
|
||||
@@ -159,7 +159,7 @@ class ExecuteActions(LogCaptureTestCase):
|
||||
"action2",
|
||||
os.path.join(TEST_FILES_DIR, "action.d/action_modifyainfo.py"),
|
||||
{})
|
||||
self.__jail.putFailTicket(FailTicket("1.2.3.4", 0))
|
||||
self.__jail.putFailTicket(FailTicket("1.2.3.4"))
|
||||
self.__actions._Actions__checkBan()
|
||||
# Will fail if modification of aInfo from first action propagates
|
||||
# to second action, as both delete same key
|
||||
@@ -217,6 +217,9 @@ class ExecuteActions(LogCaptureTestCase):
|
||||
# flush for inet6 is intentionally "broken" here - test no unhandled except and invariant check:
|
||||
act['actionflush?family=inet6'] = act.actionflush + '; exit 1'
|
||||
act.actionstart_on_demand = True
|
||||
# force errors via check in ban/unban:
|
||||
act.actionban = "<actioncheck> ; " + act.actionban
|
||||
act.actionunban = "<actioncheck> ; " + act.actionunban
|
||||
self.__actions.start()
|
||||
self.assertNotLogged("stdout: %r" % 'ip start')
|
||||
|
||||
@@ -294,6 +297,9 @@ class ExecuteActions(LogCaptureTestCase):
|
||||
act['actionflush?family=inet6'] = act.actionflush + '; exit 1'
|
||||
act.actionstart_on_demand = True
|
||||
act.actionrepair_on_unban = True
|
||||
# force errors via check in ban/unban:
|
||||
act.actionban = "<actioncheck> ; " + act.actionban
|
||||
act.actionunban = "<actioncheck> ; " + act.actionunban
|
||||
self.__actions.start()
|
||||
self.assertNotLogged("stdout: %r" % 'ip start')
|
||||
|
||||
|
||||
@@ -207,15 +207,15 @@ class CommandActionTest(LogCaptureTestCase):
|
||||
self.assertEqual(
|
||||
self.__action.replaceTag("<matches>",
|
||||
{'matches': "some >char< should \\< be[ escap}ed&\n"}),
|
||||
"some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\n")
|
||||
"some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\\n")
|
||||
self.assertEqual(
|
||||
self.__action.replaceTag("<ipmatches>",
|
||||
{'ipmatches': "some >char< should \\< be[ escap}ed&\n"}),
|
||||
"some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\n")
|
||||
"some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\\n")
|
||||
self.assertEqual(
|
||||
self.__action.replaceTag("<ipjailmatches>",
|
||||
{'ipjailmatches': "some >char< should \\< be[ escap}ed&\n"}),
|
||||
"some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\n")
|
||||
{'ipjailmatches': "some >char< should \\< be[ escap}ed&\r\n"}),
|
||||
"some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\\r\\n")
|
||||
|
||||
# Recursive
|
||||
aInfo["ABC"] = "<xyz>"
|
||||
@@ -305,8 +305,8 @@ class CommandActionTest(LogCaptureTestCase):
|
||||
self.assertEqual(self.__action.actionstart, "touch '%s'" % tmp)
|
||||
self.__action.actionstop = "rm -f '%s'" % tmp
|
||||
self.assertEqual(self.__action.actionstop, "rm -f '%s'" % tmp)
|
||||
self.__action.actionban = "echo -n"
|
||||
self.assertEqual(self.__action.actionban, 'echo -n')
|
||||
self.__action.actionban = "<actioncheck> && echo -n"
|
||||
self.assertEqual(self.__action.actionban, "<actioncheck> && echo -n")
|
||||
self.__action.actioncheck = "[ -e '%s' ]" % tmp
|
||||
self.assertEqual(self.__action.actioncheck, "[ -e '%s' ]" % tmp)
|
||||
self.__action.actionunban = "true"
|
||||
@@ -316,6 +316,7 @@ class CommandActionTest(LogCaptureTestCase):
|
||||
self.assertNotLogged('returned')
|
||||
# no action was actually executed yet
|
||||
|
||||
# start on demand is false, so it should cause failure on first attempt of ban:
|
||||
self.__action.ban({'ip': None})
|
||||
self.assertLogged('Invariant check failed')
|
||||
self.assertLogged('returned successfully')
|
||||
@@ -365,12 +366,51 @@ class CommandActionTest(LogCaptureTestCase):
|
||||
self.pruneLog('[phase 2]')
|
||||
self.__action.actionstart = "touch '%s'" % tmp
|
||||
self.__action.actionstop = "rm '%s'" % tmp
|
||||
self.__action.actionban = """printf "%%%%b\n" <ip> >> '%s'""" % tmp
|
||||
self.__action.actionban = """<actioncheck> && printf "%%%%b\n" <ip> >> '%s'""" % tmp
|
||||
self.__action.actioncheck = "[ -e '%s' ]" % tmp
|
||||
self.__action.ban({'ip': None})
|
||||
self.assertLogged('Invariant check failed')
|
||||
self.assertNotLogged('Unable to restore environment')
|
||||
|
||||
@with_tmpdir
|
||||
def testExecuteActionCheckOnBanFailure(self, tmp):
|
||||
tmp += '/fail2ban.test'
|
||||
self.__action.actionstart = "touch '%s'; echo 'started ...'" % tmp
|
||||
self.__action.actionstop = "rm -f '%s'" % tmp
|
||||
self.__action.actionban = "[ -e '%s' ] && echo 'banned '<ip>" % tmp
|
||||
self.__action.actioncheck = "[ -e '%s' ] && echo 'check ok' || { echo 'check failed'; exit 1; }" % tmp
|
||||
self.__action.actionrepair = "echo 'repair ...'; touch '%s'" % tmp
|
||||
self.__action.actionstart_on_demand = False
|
||||
self.__action.start()
|
||||
# phase 1: with repair;
|
||||
# phase 2: without repair (start/stop), not on demand;
|
||||
# phase 3: without repair (start/stop), start on demand.
|
||||
for i in (1, 2, 3):
|
||||
self.pruneLog('[phase %s]' % i)
|
||||
# 1st time with success ban:
|
||||
self.__action.ban({'ip': '192.0.2.1'})
|
||||
self.assertLogged(
|
||||
"stdout: %r" % 'banned 192.0.2.1', all=True)
|
||||
self.assertNotLogged("Invariant check failed. Trying",
|
||||
"stdout: %r" % 'check failed',
|
||||
"stdout: %r" % ('repair ...' if self.__action.actionrepair else 'started ...'),
|
||||
"stdout: %r" % 'check ok', all=True)
|
||||
# force error in ban:
|
||||
os.remove(tmp)
|
||||
self.pruneLog()
|
||||
# 2nd time with fail recognition, success repair, check and ban:
|
||||
self.__action.ban({'ip': '192.0.2.2'})
|
||||
self.assertLogged("Invariant check failed. Trying",
|
||||
"stdout: %r" % 'check failed',
|
||||
"stdout: %r" % ('repair ...' if self.__action.actionrepair else 'started ...'),
|
||||
"stdout: %r" % 'check ok',
|
||||
"stdout: %r" % 'banned 192.0.2.2', all=True)
|
||||
# repeat without repair (stop/start), herafter enable on demand:
|
||||
if self.__action.actionrepair:
|
||||
self.__action.actionrepair = ""
|
||||
elif not self.__action.actionstart_on_demand:
|
||||
self.__action.actionstart_on_demand = True
|
||||
|
||||
@with_tmpdir
|
||||
def testExecuteActionCheckRepairEnvironment(self, tmp):
|
||||
tmp += '/fail2ban.test'
|
||||
|
||||
@@ -26,19 +26,24 @@ __license__ = "GPL"
|
||||
|
||||
import unittest
|
||||
|
||||
from .utils import setUpMyTime, tearDownMyTime
|
||||
|
||||
from ..server.banmanager import BanManager
|
||||
from ..server.ipdns import DNSUtils
|
||||
from ..server.ticket import BanTicket
|
||||
|
||||
class AddFailure(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""Call before every test case."""
|
||||
super(AddFailure, self).setUp()
|
||||
setUpMyTime()
|
||||
self.__ticket = BanTicket('193.168.0.128', 1167605999.0)
|
||||
self.__banManager = BanManager()
|
||||
|
||||
def tearDown(self):
|
||||
"""Call after every test case."""
|
||||
super(AddFailure, self).tearDown()
|
||||
tearDownMyTime()
|
||||
|
||||
def testAdd(self):
|
||||
self.assertTrue(self.__banManager.addBanTicket(self.__ticket))
|
||||
@@ -93,6 +98,25 @@ class AddFailure(unittest.TestCase):
|
||||
self.assertTrue(self.__banManager.addBanTicket(self.__ticket))
|
||||
ticket = BanTicket('111.111.1.111', 1167605999.0)
|
||||
self.assertFalse(self.__banManager._inBanList(ticket))
|
||||
|
||||
def testBanTimeIncr(self):
|
||||
ticket = BanTicket(self.__ticket.getIP(), self.__ticket.getTime())
|
||||
## increase twice and at end permanent, check time/count increase:
|
||||
c = 0
|
||||
for i in (1000, 2000, -1):
|
||||
self.__banManager.addBanTicket(self.__ticket); c += 1
|
||||
ticket.setBanTime(i)
|
||||
self.assertFalse(self.__banManager.addBanTicket(ticket)); # no incr of c (already banned)
|
||||
self.assertEqual(str(self.__banManager.getTicketByID(ticket.getIP())),
|
||||
"BanTicket: ip=%s time=%s bantime=%s bancount=%s #attempts=0 matches=[]" % (ticket.getIP(), ticket.getTime(), i, c))
|
||||
## after permanent, it should remain permanent ban time (-1):
|
||||
self.__banManager.addBanTicket(self.__ticket); c += 1
|
||||
ticket.setBanTime(-1)
|
||||
self.assertFalse(self.__banManager.addBanTicket(ticket)); # no incr of c (already banned)
|
||||
ticket.setBanTime(1000)
|
||||
self.assertFalse(self.__banManager.addBanTicket(ticket)); # no incr of c (already banned)
|
||||
self.assertEqual(str(self.__banManager.getTicketByID(ticket.getIP())),
|
||||
"BanTicket: ip=%s time=%s bantime=%s bancount=%s #attempts=0 matches=[]" % (ticket.getIP(), ticket.getTime(), -1, c))
|
||||
|
||||
def testUnban(self):
|
||||
btime = self.__banManager.getBanTime()
|
||||
@@ -131,16 +155,32 @@ class AddFailure(unittest.TestCase):
|
||||
finally:
|
||||
self.__banManager.setBanTime(btime)
|
||||
|
||||
def testBanList(self):
|
||||
tickets = [
|
||||
BanTicket('192.0.2.1', 1167605999.0),
|
||||
BanTicket('192.0.2.2', 1167605999.0),
|
||||
]
|
||||
tickets[1].setBanTime(-1)
|
||||
for t in tickets:
|
||||
self.__banManager.addBanTicket(t)
|
||||
self.assertSortedEqual(self.__banManager.getBanList(ordered=True, withTime=True),
|
||||
[
|
||||
'192.0.2.1 \t2006-12-31 23:59:59 + 600 = 2007-01-01 00:09:59',
|
||||
'192.0.2.2 \t2006-12-31 23:59:59 + -1 = 9999-12-31 23:59:59'
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class StatusExtendedCymruInfo(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""Call before every test case."""
|
||||
super(StatusExtendedCymruInfo, self).setUp()
|
||||
unittest.F2B.SkipIfNoNetwork()
|
||||
self.__ban_ip = "93.184.216.34"
|
||||
self.__asn = "15133"
|
||||
self.__country = "EU"
|
||||
self.__rir = "ripencc"
|
||||
setUpMyTime()
|
||||
self.__ban_ip = iter(DNSUtils.dnsToIp("resolver1.opendns.com")).next()
|
||||
self.__asn = "36692"
|
||||
self.__country = "US"
|
||||
self.__rir = "arin"
|
||||
ticket = BanTicket(self.__ban_ip, 1167605999.0)
|
||||
self.__banManager = BanManager()
|
||||
self.assertTrue(self.__banManager.addBanTicket(ticket))
|
||||
@@ -148,6 +188,7 @@ class StatusExtendedCymruInfo(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
"""Call after every test case."""
|
||||
super(StatusExtendedCymruInfo, self).tearDown()
|
||||
tearDownMyTime()
|
||||
|
||||
available = True, None
|
||||
|
||||
|
||||
@@ -381,13 +381,16 @@ class JailReaderTest(LogCaptureTestCase):
|
||||
self.assertEqual(('mail.who_is', {'a':'cat', 'b':'dog'}), extractOptions("mail.who_is[a=cat,b=dog]"))
|
||||
self.assertEqual(('mail--ho_is', {}), extractOptions("mail--ho_is"))
|
||||
|
||||
self.assertEqual(('mail--ho_is', {}), extractOptions("mail--ho_is['s']"))
|
||||
#print(self.getLog())
|
||||
#self.assertLogged("Invalid argument ['s'] in ''s''")
|
||||
|
||||
self.assertEqual(('mail', {'a': ','}), extractOptions("mail[a=',']"))
|
||||
self.assertEqual(('mail', {'a': 'b'}), extractOptions("mail[a=b, ]"))
|
||||
|
||||
#self.assertRaises(ValueError, extractOptions ,'mail-how[')
|
||||
self.assertRaises(ValueError, extractOptions ,'mail-how[')
|
||||
|
||||
self.assertRaises(ValueError, extractOptions, """mail[a="test with interim (wrong) "" quotes"]""")
|
||||
self.assertRaises(ValueError, extractOptions, """mail[a='test with interim (wrong) '' quotes']""")
|
||||
self.assertRaises(ValueError, extractOptions, """mail[a='x, y, z', b=x, y, z]""")
|
||||
|
||||
self.assertRaises(ValueError, extractOptions, """mail['s']""")
|
||||
|
||||
# Empty option
|
||||
option = "abc[]"
|
||||
@@ -455,8 +458,6 @@ class JailReaderTest(LogCaptureTestCase):
|
||||
('sender', 'f2b-test@example.com'), ('blocklist_de_apikey', 'test-key'),
|
||||
('action',
|
||||
'%(action_blocklist_de)s\n'
|
||||
'%(action_badips_report)s\n'
|
||||
'%(action_badips)s\n'
|
||||
'mynetwatchman[port=1234,protocol=udp,agent="%(fail2ban_agent)s"]'
|
||||
),
|
||||
))
|
||||
@@ -470,16 +471,14 @@ class JailReaderTest(LogCaptureTestCase):
|
||||
if len(cmd) <= 4:
|
||||
continue
|
||||
# differentiate between set and multi-set (wrop it here to single set):
|
||||
if cmd[0] == 'set' and (cmd[4] == 'agent' or cmd[4].endswith('badips.py')):
|
||||
if cmd[0] == 'set' and cmd[4] == 'agent':
|
||||
act.append(cmd)
|
||||
elif cmd[0] == 'multi-set':
|
||||
act.extend([['set'] + cmd[1:4] + o for o in cmd[4] if o[0] == 'agent'])
|
||||
useragent = 'Fail2Ban/%s' % version
|
||||
self.assertEqual(len(act), 4)
|
||||
self.assertEqual(len(act), 2)
|
||||
self.assertEqual(act[0], ['set', 'blocklisttest', 'action', 'blocklist_de', 'agent', useragent])
|
||||
self.assertEqual(act[1], ['set', 'blocklisttest', 'action', 'badips', 'agent', useragent])
|
||||
self.assertEqual(eval(act[2][5]).get('agent', '<wrong>'), useragent)
|
||||
self.assertEqual(act[3], ['set', 'blocklisttest', 'action', 'mynetwatchman', 'agent', useragent])
|
||||
self.assertEqual(act[1], ['set', 'blocklisttest', 'action', 'mynetwatchman', 'agent', useragent])
|
||||
|
||||
@with_tmpdir
|
||||
def testGlob(self, d):
|
||||
@@ -752,9 +751,9 @@ class JailsReaderTest(LogCaptureTestCase):
|
||||
['add', 'tz_correct', 'auto'],
|
||||
['start', 'tz_correct'],
|
||||
['config-error',
|
||||
"Jail 'brokenactiondef' skipped, because of wrong configuration: Invalid action definition 'joho[foo'"],
|
||||
"Jail 'brokenactiondef' skipped, because of wrong configuration: Invalid action definition 'joho[foo': unexpected option syntax"],
|
||||
['config-error',
|
||||
"Jail 'brokenfilterdef' skipped, because of wrong configuration: Invalid filter definition 'flt[test'"],
|
||||
"Jail 'brokenfilterdef' skipped, because of wrong configuration: Invalid filter definition 'flt[test': unexpected option syntax"],
|
||||
['config-error',
|
||||
"Jail 'missingaction' skipped, because of wrong configuration: Unable to read action 'noactionfileforthisaction'"],
|
||||
['config-error',
|
||||
@@ -975,6 +974,7 @@ class JailsReaderTest(LogCaptureTestCase):
|
||||
['set', 'syslogsocket', 'auto'],
|
||||
['set', 'loglevel', "INFO"],
|
||||
['set', 'logtarget', '/var/log/fail2ban.log'],
|
||||
['set', 'allowipv6', 'auto'],
|
||||
['set', 'dbfile', '/var/lib/fail2ban/fail2ban.sqlite3'],
|
||||
['set', 'dbmaxmatches', 10],
|
||||
['set', 'dbpurgeage', '1d'],
|
||||
|
||||
@@ -29,7 +29,7 @@ import tempfile
|
||||
import sqlite3
|
||||
import shutil
|
||||
|
||||
from ..server.filter import FileContainer
|
||||
from ..server.filter import FileContainer, Filter
|
||||
from ..server.mytime import MyTime
|
||||
from ..server.ticket import FailTicket
|
||||
from ..server.actions import Actions, Utils
|
||||
@@ -164,10 +164,47 @@ class DatabaseTest(LogCaptureTestCase):
|
||||
|
||||
self.assertEqual(self.db.updateDb(Fail2BanDb.__version__), Fail2BanDb.__version__)
|
||||
self.assertRaises(NotImplementedError, self.db.updateDb, Fail2BanDb.__version__ + 1)
|
||||
# check current bans (should find exactly 1 ticket after upgrade):
|
||||
tickets = self.db.getCurrentBans(fromtime=1388009242, correctBanTime=123456)
|
||||
self.assertEqual(len(tickets), 1)
|
||||
self.assertEqual(tickets[0].getBanTime(), 123456); # ban-time was unknown (normally updated from jail)
|
||||
finally:
|
||||
if self.db and self.db._dbFilename != ":memory:":
|
||||
os.remove(self.db._dbBackupFilename)
|
||||
|
||||
def testUpdateDb2(self):
|
||||
self.db = None
|
||||
if self.dbFilename is None: # pragma: no cover
|
||||
_, self.dbFilename = tempfile.mkstemp(".db", "fail2ban_")
|
||||
shutil.copyfile(
|
||||
os.path.join(TEST_FILES_DIR, 'database_v2.db'), self.dbFilename)
|
||||
self.db = Fail2BanDb(self.dbFilename)
|
||||
self.assertEqual(self.db.getJailNames(), set(['pam-generic']))
|
||||
self.assertEqual(self.db.getLogPaths(), set(['/var/log/auth.log']))
|
||||
bans = self.db.getBans()
|
||||
self.assertEqual(len(bans), 2)
|
||||
# compare first ticket completely:
|
||||
ticket = FailTicket("1.2.3.7", 1417595494, [
|
||||
u'Dec 3 09:31:08 f2btest test:auth[27658]: pam_unix(test:auth): authentication failure; logname= uid=0 euid=0 tty=test ruser= rhost=1.2.3.7',
|
||||
u'Dec 3 09:31:32 f2btest test:auth[27671]: pam_unix(test:auth): authentication failure; logname= uid=0 euid=0 tty=test ruser= rhost=1.2.3.7',
|
||||
u'Dec 3 09:31:34 f2btest test:auth[27673]: pam_unix(test:auth): authentication failure; logname= uid=0 euid=0 tty=test ruser= rhost=1.2.3.7'
|
||||
])
|
||||
ticket.setAttempt(3)
|
||||
self.assertEqual(bans[0], ticket)
|
||||
# second ban found also:
|
||||
self.assertEqual(bans[1].getIP(), "1.2.3.8")
|
||||
# updated ?
|
||||
self.assertEqual(self.db.updateDb(Fail2BanDb.__version__), Fail2BanDb.__version__)
|
||||
# check current bans (should find 2 tickets after upgrade):
|
||||
self.jail = DummyJail(name='pam-generic')
|
||||
tickets = self.db.getCurrentBans(jail=self.jail, fromtime=1417595494)
|
||||
self.assertEqual(len(tickets), 2)
|
||||
self.assertEqual(tickets[0].getBanTime(), 600)
|
||||
# further update should fail:
|
||||
self.assertRaises(NotImplementedError, self.db.updateDb, Fail2BanDb.__version__ + 1)
|
||||
# clean:
|
||||
os.remove(self.db._dbBackupFilename)
|
||||
|
||||
def testAddJail(self):
|
||||
self.jail = DummyJail()
|
||||
self.db.addJail(self.jail)
|
||||
@@ -175,19 +212,20 @@ class DatabaseTest(LogCaptureTestCase):
|
||||
self.jail.name in self.db.getJailNames(True),
|
||||
"Jail not added to database")
|
||||
|
||||
def testAddLog(self):
|
||||
def _testAddLog(self):
|
||||
self.testAddJail() # Jail required
|
||||
|
||||
_, filename = tempfile.mkstemp(".log", "Fail2BanDb_")
|
||||
self.fileContainer = FileContainer(filename, "utf-8")
|
||||
|
||||
self.db.addLog(self.jail, self.fileContainer)
|
||||
pos = self.db.addLog(self.jail, self.fileContainer)
|
||||
self.assertTrue(pos is None); # unknown previously
|
||||
|
||||
self.assertIn(filename, self.db.getLogPaths(self.jail))
|
||||
os.remove(filename)
|
||||
|
||||
def testUpdateLog(self):
|
||||
self.testAddLog() # Add log file
|
||||
self._testAddLog() # Add log file
|
||||
|
||||
# Write some text
|
||||
filename = self.fileContainer.getFileName()
|
||||
@@ -400,7 +438,7 @@ class DatabaseTest(LogCaptureTestCase):
|
||||
def testGetBansMerged(self):
|
||||
self.testAddJail()
|
||||
|
||||
jail2 = DummyJail()
|
||||
jail2 = DummyJail(name='DummyJail-2')
|
||||
self.db.addJail(jail2)
|
||||
|
||||
ticket = FailTicket("127.0.0.1", MyTime.time() - 40, ["abc\n"])
|
||||
@@ -482,26 +520,46 @@ class DatabaseTest(LogCaptureTestCase):
|
||||
tickets = self.db.getCurrentBans(jail=self.jail, forbantime=15,
|
||||
fromtime=MyTime.time() + MyTime.str2seconds("1year"))
|
||||
self.assertEqual(len(tickets), 0)
|
||||
# persistent bantime (-1), so never expired:
|
||||
# persistent bantime (-1), so never expired (but no persistent tickets):
|
||||
tickets = self.db.getCurrentBans(jail=self.jail, forbantime=-1,
|
||||
fromtime=MyTime.time() + MyTime.str2seconds("1year"))
|
||||
self.assertEqual(len(tickets), 2)
|
||||
self.assertEqual(len(tickets), 0)
|
||||
# add persistent one:
|
||||
ticket.setBanTime(-1)
|
||||
self.db.addBan(self.jail, ticket)
|
||||
# persistent bantime (-1), so never expired (but jail has other max bantime now):
|
||||
tickets = self.db.getCurrentBans(jail=self.jail, forbantime=-1,
|
||||
fromtime=MyTime.time() + MyTime.str2seconds("1year"))
|
||||
# no tickets should be found (max ban time = 600):
|
||||
self.assertEqual(len(tickets), 0)
|
||||
self.assertLogged("ignore ticket (with new max ban-time %r)" % self.jail.getMaxBanTime())
|
||||
# change jail to persistent ban and try again (1 persistent ticket):
|
||||
self.jail.actions.setBanTime(-1)
|
||||
tickets = self.db.getCurrentBans(jail=self.jail, forbantime=-1,
|
||||
fromtime=MyTime.time() + MyTime.str2seconds("1year"))
|
||||
self.assertEqual(len(tickets), 1)
|
||||
self.assertEqual(tickets[0].getBanTime(), -1); # current jail ban time.
|
||||
|
||||
def testActionWithDB(self):
|
||||
# test action together with database functionality
|
||||
self.testAddJail() # Jail required
|
||||
self.jail.database = self.db
|
||||
self.db.addJail(self.jail)
|
||||
actions = Actions(self.jail)
|
||||
actions = self.jail.actions
|
||||
actions.add(
|
||||
"action_checkainfo",
|
||||
os.path.join(TEST_FILES_DIR, "action.d/action_checkainfo.py"),
|
||||
{})
|
||||
ticket = FailTicket("1.2.3.4", MyTime.time(), ['test', 'test'])
|
||||
actions.banManager.setBanTotal(20)
|
||||
self.jail._Jail__filter = flt = Filter(self.jail)
|
||||
flt.failManager.setFailTotal(50)
|
||||
ticket = FailTicket("1.2.3.4")
|
||||
ticket.setAttempt(5)
|
||||
ticket.setMatches(['test', 'test'])
|
||||
self.jail.putFailTicket(ticket)
|
||||
actions._Actions__checkBan()
|
||||
self.assertLogged("ban ainfo %s, %s, %s, %s" % (True, True, True, True))
|
||||
self.assertLogged("jail info %d, %d, %d, %d" % (1, 21, 0, 50))
|
||||
|
||||
def testDelAndAddJail(self):
|
||||
self.testAddJail() # Add jail
|
||||
|
||||
@@ -516,6 +516,9 @@ class CustomDateFormatsTest(unittest.TestCase):
|
||||
(1072746123.0 - 3600, "{^LN-BEG}%ExY-%Exm-%Exd %ExH:%ExM:%ExS(?: %Z)?", "[2003-12-30 01:02:03] server ..."),
|
||||
(1072746123.0, "{^LN-BEG}%ExY-%Exm-%Exd %ExH:%ExM:%ExS(?: %z)?", "[2003-12-30 01:02:03 UTC] server ..."),
|
||||
(1072746123.0, "{^LN-BEG}%ExY-%Exm-%Exd %ExH:%ExM:%ExS(?: %Z)?", "[2003-12-30 01:02:03 UTC] server ..."),
|
||||
(1072746123.0, "{^LN-BEG}%ExY-%Exm-%Exd %ExH:%ExM:%ExS(?: %z)?", "[2003-12-30 01:02:03 Z] server ..."),
|
||||
(1072746123.0, "{^LN-BEG}%ExY-%Exm-%Exd %ExH:%ExM:%ExS(?: %z)?", "[2003-12-30 01:02:03 +0000] server ..."),
|
||||
(1072746123.0, "{^LN-BEG}%ExY-%Exm-%Exd %ExH:%ExM:%ExS(?: %Z)?", "[2003-12-30 01:02:03 Z] server ..."),
|
||||
):
|
||||
logSys.debug('== test: %r', (matched, dp, line))
|
||||
if dp is None:
|
||||
@@ -551,6 +554,9 @@ class CustomDateFormatsTest(unittest.TestCase):
|
||||
(1123970401.0, "^%ExH:%ExM:%ExS**", '00:00:01'),
|
||||
# cover date with current year, in test cases now == Aug 2005 -> back to last year (Sep 2004):
|
||||
(1094068799.0, "^%m/%d %ExH:%ExM:%ExS**", '09/01 21:59:59'),
|
||||
# no time (only date) in pattern, assume local 00:00:00 for H:M:S :
|
||||
(1093989600.0, "^%Y-%m-%d**", '2004-09-01'),
|
||||
(1093996800.0, "^%Y-%m-%d%z**", '2004-09-01Z'),
|
||||
):
|
||||
logSys.debug('== test: %r', (matched, dp, line))
|
||||
dd = DateDetector()
|
||||
|
||||
@@ -28,14 +28,19 @@ from ..server.jail import Jail
|
||||
from ..server.actions import Actions
|
||||
|
||||
|
||||
class DummyActions(Actions):
|
||||
def checkBan(self):
|
||||
return self._Actions__checkBan()
|
||||
|
||||
|
||||
class DummyJail(Jail):
|
||||
"""A simple 'jail' to suck in all the tickets generated by Filter's
|
||||
"""
|
||||
def __init__(self, backend=None):
|
||||
def __init__(self, name='DummyJail', backend=None):
|
||||
self.lock = Lock()
|
||||
self.queue = []
|
||||
super(DummyJail, self).__init__(name='DummyJail', backend=backend)
|
||||
self.__actions = Actions(self)
|
||||
super(DummyJail, self).__init__(name=name, backend=backend)
|
||||
self.__actions = DummyActions(self)
|
||||
|
||||
def __len__(self):
|
||||
with self.lock:
|
||||
@@ -64,10 +69,6 @@ class DummyJail(Jail):
|
||||
except IndexError:
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return "DummyJail" + ("" if self.database else " #%s with %d tickets" % (id(self), len(self)))
|
||||
|
||||
@property
|
||||
def idle(self):
|
||||
return False;
|
||||
|
||||
@@ -44,7 +44,7 @@ from ..server import server
|
||||
from ..server.mytime import MyTime
|
||||
from ..server.utils import Utils
|
||||
from .utils import LogCaptureTestCase, logSys as DefLogSys, with_tmpdir, shutil, logging, \
|
||||
STOCK, CONFIG_DIR as STOCK_CONF_DIR
|
||||
STOCK, CONFIG_DIR as STOCK_CONF_DIR, TEST_NOW, tearDownMyTime
|
||||
|
||||
from ..helpers import getLogger
|
||||
|
||||
@@ -78,6 +78,35 @@ fail2banclient.output = \
|
||||
fail2banserver.output = \
|
||||
protocol.output = _test_output
|
||||
|
||||
def _time_shift(shift):
|
||||
# jump to the future (+shift minutes):
|
||||
logSys.debug("===>>> time shift + %s min", shift)
|
||||
MyTime.setTime(MyTime.time() + shift*60)
|
||||
|
||||
|
||||
Observers = server.Observers
|
||||
|
||||
def _observer_wait_idle():
|
||||
"""Helper to wait observer becomes idle"""
|
||||
if Observers.Main is not None:
|
||||
Observers.Main.wait_empty(MID_WAITTIME)
|
||||
Observers.Main.wait_idle(MID_WAITTIME / 5)
|
||||
|
||||
def _observer_wait_before_incrban(cond, timeout=MID_WAITTIME):
|
||||
"""Helper to block observer before increase bantime until some condition gets true"""
|
||||
if Observers.Main is not None:
|
||||
# switch ban handler:
|
||||
_obs_banFound = Observers.Main.banFound
|
||||
def _banFound(*args, **kwargs):
|
||||
# restore original handler:
|
||||
Observers.Main.banFound = _obs_banFound
|
||||
# wait for:
|
||||
logSys.debug(' [Observer::banFound] *** observer blocked for test')
|
||||
Utils.wait_for(cond, timeout)
|
||||
logSys.debug(' [Observer::banFound] +++ observer runs again')
|
||||
# original banFound:
|
||||
_obs_banFound(*args, **kwargs)
|
||||
Observers.Main.banFound = _banFound
|
||||
|
||||
#
|
||||
# Mocking .exit so we could test its correct operation.
|
||||
@@ -201,7 +230,7 @@ def _start_params(tmp, use_stock=False, use_stock_cfg=None,
|
||||
os.symlink(os.path.abspath(pjoin(STOCK_CONF_DIR, n)), pjoin(cfg, n))
|
||||
if create_before_start:
|
||||
for n in create_before_start:
|
||||
_write_file(n % {'tmp': tmp}, 'w', '')
|
||||
_write_file(n % {'tmp': tmp}, 'w')
|
||||
# parameters (sock/pid and config, increase verbosity, set log, etc.):
|
||||
vvv, llev = (), "INFO"
|
||||
if unittest.F2B.log_level < logging.INFO: # pragma: no cover
|
||||
@@ -314,6 +343,7 @@ def with_foreground_server_thread(startextra={}):
|
||||
# to wait for end of server, default accept any exit code, because multi-threaded,
|
||||
# thus server can exit in-between...
|
||||
def _stopAndWaitForServerEnd(code=(SUCCESS, FAILED)):
|
||||
tearDownMyTime()
|
||||
# if seems to be down - try to catch end phase (wait a bit for end:True to recognize down state):
|
||||
if not phase.get('end', None) and not os.path.exists(pjoin(tmp, "f2b.pid")):
|
||||
Utils.wait_for(lambda: phase.get('end', None) is not None, MID_WAITTIME)
|
||||
@@ -353,6 +383,7 @@ def with_foreground_server_thread(startextra={}):
|
||||
# so don't kill (same process) - if success, just wait for end of worker:
|
||||
if phase.get('end', None):
|
||||
th.join()
|
||||
tearDownMyTime()
|
||||
return wrapper
|
||||
return _deco_wrapper
|
||||
|
||||
@@ -379,6 +410,7 @@ class Fail2banClientServerBase(LogCaptureTestCase):
|
||||
server.DEF_LOGTARGET = SRV_DEF_LOGTARGET
|
||||
server.DEF_LOGLEVEL = SRV_DEF_LOGLEVEL
|
||||
LogCaptureTestCase.tearDown(self)
|
||||
tearDownMyTime()
|
||||
|
||||
@staticmethod
|
||||
def _test_exit(code=0):
|
||||
@@ -446,14 +478,14 @@ class Fail2banClientServerBase(LogCaptureTestCase):
|
||||
|
||||
@with_foreground_server_thread(startextra={'f2b_local':(
|
||||
"[Thread]",
|
||||
"stacksize = 32"
|
||||
"stacksize = 128"
|
||||
"",
|
||||
)})
|
||||
def testStartForeground(self, tmp, startparams):
|
||||
# check thread options were set:
|
||||
self.pruneLog()
|
||||
self.execCmd(SUCCESS, startparams, "get", "thread")
|
||||
self.assertLogged("{'stacksize': 32}")
|
||||
self.assertLogged("{'stacksize': 128}")
|
||||
# several commands to server:
|
||||
self.execCmd(SUCCESS, startparams, "ping")
|
||||
self.execCmd(FAILED, startparams, "~~unknown~cmd~failed~~")
|
||||
@@ -905,10 +937,8 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
||||
"Jail 'broken-jail' skipped, because of wrong configuration", all=True)
|
||||
|
||||
# enable both jails, 3 logs for jail1, etc...
|
||||
# truncate test-log - we should not find unban/ban again by reload:
|
||||
self.pruneLog("[test-phase 1b]")
|
||||
_write_jail_cfg(actions=[1,2])
|
||||
_write_file(test1log, "w+")
|
||||
if unittest.F2B.log_level < logging.DEBUG: # pragma: no cover
|
||||
_out_file(test1log)
|
||||
self.execCmd(SUCCESS, startparams, "reload")
|
||||
@@ -971,7 +1001,7 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
||||
|
||||
self.pruneLog("[test-phase 2b]")
|
||||
# write new failures:
|
||||
_write_file(test2log, "w+", *(
|
||||
_write_file(test2log, "a+", *(
|
||||
(str(int(MyTime.time())) + " error 403 from 192.0.2.2: test 2",) * 3 +
|
||||
(str(int(MyTime.time())) + " error 403 from 192.0.2.3: test 2",) * 3 +
|
||||
(str(int(MyTime.time())) + " failure 401 from 192.0.2.4: test 2",) * 3 +
|
||||
@@ -1003,6 +1033,8 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
||||
"[test-jail2] Found 192.0.2.3",
|
||||
"[test-jail2] Ban 192.0.2.3",
|
||||
all=True)
|
||||
# if observer available wait for it becomes idle (write all tickets to db):
|
||||
_observer_wait_idle()
|
||||
# test banned command:
|
||||
self.assertSortedEqual(self.execCmdDirect(startparams,
|
||||
'banned'), (0, [
|
||||
@@ -1028,10 +1060,6 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
||||
self.assertEqual(self.execCmdDirect(startparams,
|
||||
'get', 'test-jail1', 'banned', '192.0.2.3', '192.0.2.9')[1], [1, 0])
|
||||
|
||||
# rotate logs:
|
||||
_write_file(test1log, "w+")
|
||||
_write_file(test2log, "w+")
|
||||
|
||||
# restart jail without unban all:
|
||||
self.pruneLog("[test-phase 2c]")
|
||||
self.execCmd(SUCCESS, startparams,
|
||||
@@ -1072,6 +1100,17 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
||||
"stdout: '[test-jail2] test-action3: ++ ban 192.0.2.22",
|
||||
"stdout: '[test-jail2] test-action3: ++ ban 192.0.2.22 ", all=True, wait=MID_WAITTIME)
|
||||
|
||||
# get banned ips:
|
||||
_observer_wait_idle()
|
||||
self.pruneLog("[test-phase 2d.1]")
|
||||
self.execCmd(SUCCESS, startparams, "get", "test-jail2", "banip", "\n")
|
||||
self.assertLogged(
|
||||
"192.0.2.4", "192.0.2.8", "192.0.2.21", "192.0.2.22", all=True, wait=MID_WAITTIME)
|
||||
self.pruneLog("[test-phase 2d.2]")
|
||||
self.execCmd(SUCCESS, startparams, "get", "test-jail1", "banip")
|
||||
self.assertLogged(
|
||||
"192.0.2.1", "192.0.2.2", "192.0.2.3", "192.0.2.4", "192.0.2.8", all=True, wait=MID_WAITTIME)
|
||||
|
||||
# restart jail with unban all:
|
||||
self.pruneLog("[test-phase 2e]")
|
||||
self.execCmd(SUCCESS, startparams,
|
||||
@@ -1138,7 +1177,7 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
||||
|
||||
# now write failures again and check already banned (jail1 was alive the whole time) and new bans occurred (jail1 was alive the whole time):
|
||||
self.pruneLog("[test-phase 5]")
|
||||
_write_file(test1log, "w+", *(
|
||||
_write_file(test1log, "a+", *(
|
||||
(str(int(MyTime.time())) + " failure 401 from 192.0.2.1: test 5",) * 3 +
|
||||
(str(int(MyTime.time())) + " error 403 from 192.0.2.5: test 5",) * 3 +
|
||||
(str(int(MyTime.time())) + " failure 401 from 192.0.2.6: test 5",) * 3
|
||||
@@ -1281,7 +1320,7 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
||||
'backend = polling',
|
||||
'usedns = no',
|
||||
'logpath = %(tmp)s/blck-failures.log',
|
||||
'action = nginx-block-map[blck_lst_reload="", blck_lst_file="%(tmp)s/blck-lst.map"]',
|
||||
'action = nginx-block-map[srv_cmd="echo nginx", srv_pid="%(tmp)s/f2b.pid", blck_lst_file="%(tmp)s/blck-lst.map"]',
|
||||
' blocklist_de[actionban=\'curl() { echo "*** curl" "$*";}; <Definition/actionban>\', email="Fail2Ban <fail2ban@localhost>", '
|
||||
'apikey="TEST-API-KEY", agent="fail2ban-test-agent", service=<name>]',
|
||||
'filter =',
|
||||
@@ -1289,7 +1328,7 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
||||
'failregex = ^ failure "<F-ID>[^"]+</F-ID>" - <ADDR>',
|
||||
'maxretry = 1', # ban by first failure
|
||||
'enabled = true',
|
||||
)
|
||||
)
|
||||
})
|
||||
def testServerActions_NginxBlockMap(self, tmp, startparams):
|
||||
cfg = pjoin(tmp, "config")
|
||||
@@ -1321,6 +1360,8 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
||||
self.assertIn('\\125-000-004 1;\n', mp)
|
||||
self.assertIn('\\125-000-005 1;\n', mp)
|
||||
|
||||
# check nginx reload is logged (pid of fail2ban is used to simulate success check nginx is running):
|
||||
self.assertLogged("stdout: 'nginx -qt'", "stdout: 'nginx -s reload'", all=True)
|
||||
# check blocklist_de substitution (e. g. new-line after <matches>):
|
||||
self.assertLogged(
|
||||
"stdout: '*** curl --fail --data-urlencode server=Fail2Ban <fail2ban@localhost>"
|
||||
@@ -1363,8 +1404,9 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
||||
'jails': (
|
||||
# default:
|
||||
'''test_action = dummy[actionstart_on_demand=1, init="start: %(__name__)s", target="%(tmp)s/test.txt",
|
||||
actionban='<known/actionban>;
|
||||
echo "<matches>"; printf "=====\\n%%b\\n=====\\n\\n" "<matches>" >> <target>']''',
|
||||
actionban='<known/actionban>; echo "found: <jail.found> / <jail.found_total>, banned: <jail.banned> / <jail.banned_total>"
|
||||
echo "<matches>"; printf "=====\\n%%b\\n=====\\n\\n" "<matches>" >> <target>',
|
||||
actionstop='<known/actionstop>; echo "stats <name> - found: <jail.found_total>, banned: <jail.banned_total>"']''',
|
||||
# jail sendmail-auth:
|
||||
'[sendmail-auth]',
|
||||
'backend = polling',
|
||||
@@ -1409,7 +1451,8 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
||||
_write_file(lgfn, "w+", *smaut_msg)
|
||||
# wait and check it caused banned (and dump in the test-file):
|
||||
self.assertLogged(
|
||||
"[sendmail-auth] Ban 192.0.2.1", "1 ticket(s) in 'sendmail-auth'", all=True, wait=MID_WAITTIME)
|
||||
"[sendmail-auth] Ban 192.0.2.1", "stdout: 'found: 0 / 3, banned: 1 / 1'",
|
||||
"1 ticket(s) in 'sendmail-auth'", all=True, wait=MID_WAITTIME)
|
||||
_out_file(tofn)
|
||||
td = _read_file(tofn)
|
||||
# check matches (maxmatches = 2, so only 2 & 3 available):
|
||||
@@ -1420,10 +1463,11 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
||||
|
||||
self.pruneLog("[test-phase sendmail-reject]")
|
||||
# write log:
|
||||
_write_file(lgfn, "w+", *smrej_msg)
|
||||
_write_file(lgfn, "a+", *smrej_msg)
|
||||
# wait and check it caused banned (and dump in the test-file):
|
||||
self.assertLogged(
|
||||
"[sendmail-reject] Ban 192.0.2.2", "1 ticket(s) in 'sendmail-reject'", all=True, wait=MID_WAITTIME)
|
||||
"[sendmail-reject] Ban 192.0.2.2", "stdout: 'found: 0 / 3, banned: 1 / 1'",
|
||||
"1 ticket(s) in 'sendmail-reject'", all=True, wait=MID_WAITTIME)
|
||||
_out_file(tofn)
|
||||
td = _read_file(tofn)
|
||||
# check matches (no maxmatches, so all matched messages are available):
|
||||
@@ -1437,6 +1481,8 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
||||
# wait a bit:
|
||||
self.assertLogged(
|
||||
"Reload finished.",
|
||||
"stdout: 'stats sendmail-auth - found: 3, banned: 1'",
|
||||
"stdout: 'stats sendmail-reject - found: 3, banned: 1'",
|
||||
"[sendmail-auth] Restore Ban 192.0.2.1", "1 ticket(s) in 'sendmail-auth'", all=True, wait=MID_WAITTIME)
|
||||
# check matches again - (dbmaxmatches = 1), so it should be only last match after restart:
|
||||
td = _read_file(tofn)
|
||||
@@ -1464,6 +1510,152 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
||||
# just to debug actionstop:
|
||||
self.assertFalse(exists(tofn))
|
||||
|
||||
@with_foreground_server_thread()
|
||||
def testServerObserver(self, tmp, startparams):
|
||||
cfg = pjoin(tmp, "config")
|
||||
test1log = pjoin(tmp, "test1.log")
|
||||
|
||||
os.mkdir(pjoin(cfg, "action.d"))
|
||||
def _write_action_cfg(actname="test-action1", prolong=True):
|
||||
fn = pjoin(cfg, "action.d", "%s.conf" % actname)
|
||||
_write_file(fn, "w",
|
||||
"[DEFAULT]",
|
||||
"",
|
||||
"[Definition]",
|
||||
"actionban = printf %%s \"[%(name)s] %(actname)s: ++ ban <ip> -c <bancount> -t <bantime> : <F-MSG>\"", \
|
||||
"actionprolong = printf %%s \"[%(name)s] %(actname)s: ++ prolong <ip> -c <bancount> -t <bantime> : <F-MSG>\"" \
|
||||
if prolong else "",
|
||||
"actionunban = printf %%b '[%(name)s] %(actname)s: -- unban <ip>'",
|
||||
)
|
||||
if unittest.F2B.log_level <= logging.DEBUG: # pragma: no cover
|
||||
_out_file(fn)
|
||||
|
||||
def _write_jail_cfg(backend="polling"):
|
||||
_write_file(pjoin(cfg, "jail.conf"), "w",
|
||||
"[INCLUDES]", "",
|
||||
"[DEFAULT]", "",
|
||||
"usedns = no",
|
||||
"maxretry = 3",
|
||||
"findtime = 1m",
|
||||
"bantime = 5m",
|
||||
"bantime.increment = true",
|
||||
"datepattern = {^LN-BEG}EPOCH",
|
||||
"",
|
||||
"[test-jail1]", "backend = " + backend, "filter =",
|
||||
"action = test-action1[name='%(__name__)s']",
|
||||
" test-action2[name='%(__name__)s']",
|
||||
"logpath = " + test1log,
|
||||
r"failregex = ^\s*failure <F-ERRCODE>401|403</F-ERRCODE> from <HOST>:\s*<F-MSG>.*</F-MSG>$",
|
||||
"enabled = true",
|
||||
"",
|
||||
)
|
||||
if unittest.F2B.log_level <= logging.DEBUG: # pragma: no cover
|
||||
_out_file(pjoin(cfg, "jail.conf"))
|
||||
|
||||
# create test config:
|
||||
_write_action_cfg(actname="test-action1", prolong=False)
|
||||
_write_action_cfg(actname="test-action2", prolong=True)
|
||||
_write_jail_cfg()
|
||||
|
||||
_write_file(test1log, "w")
|
||||
# initial start:
|
||||
self.pruneLog("[test-phase 0) time-0]")
|
||||
self.execCmd(SUCCESS, startparams, "reload")
|
||||
# generate bad ip:
|
||||
_write_file(test1log, "w+", *(
|
||||
(str(int(MyTime.time())) + " failure 401 from 192.0.2.11: I'm bad \"hacker\" `` $(echo test)",) * 3
|
||||
))
|
||||
# wait for ban:
|
||||
_observer_wait_idle()
|
||||
self.assertLogged(
|
||||
"stdout: '[test-jail1] test-action1: ++ ban 192.0.2.11 -c 1 -t 300 : ",
|
||||
"stdout: '[test-jail1] test-action2: ++ ban 192.0.2.11 -c 1 -t 300 : ",
|
||||
all=True, wait=MID_WAITTIME)
|
||||
# wait for observer idle (write all tickets to db):
|
||||
_observer_wait_idle()
|
||||
|
||||
self.pruneLog("[test-phase 1) time+10m]")
|
||||
# jump to the future (+10 minutes):
|
||||
_time_shift(10)
|
||||
_observer_wait_idle()
|
||||
self.assertLogged(
|
||||
"stdout: '[test-jail1] test-action1: -- unban 192.0.2.11",
|
||||
"stdout: '[test-jail1] test-action2: -- unban 192.0.2.11",
|
||||
"0 ticket(s) in 'test-jail1'",
|
||||
all=True, wait=MID_WAITTIME)
|
||||
_observer_wait_idle()
|
||||
|
||||
self.pruneLog("[test-phase 2) time+10m]")
|
||||
# following tests are time-related - observer can prolong ticket (increase ban-time)
|
||||
# before banning, so block it here before banFound called, prolong case later:
|
||||
wakeObs = False
|
||||
_observer_wait_before_incrban(lambda: wakeObs)
|
||||
# write again (IP already bad):
|
||||
_write_file(test1log, "a+", *(
|
||||
(str(int(MyTime.time())) + " failure 401 from 192.0.2.11: I'm very bad \"hacker\" `` $(echo test)",) * 2
|
||||
))
|
||||
# wait for ban:
|
||||
self.assertLogged(
|
||||
"stdout: '[test-jail1] test-action1: ++ ban 192.0.2.11 -c 2 -t 300 : ",
|
||||
"stdout: '[test-jail1] test-action2: ++ ban 192.0.2.11 -c 2 -t 300 : ",
|
||||
all=True, wait=MID_WAITTIME)
|
||||
# get banned ips with time:
|
||||
self.pruneLog("[test-phase 2) time+10m - get-ips]")
|
||||
self.execCmd(SUCCESS, startparams, "get", "test-jail1", "banip", "--with-time")
|
||||
self.assertLogged(
|
||||
"192.0.2.11", "+ 300 =", all=True, wait=MID_WAITTIME)
|
||||
# unblock observer here and wait it is done:
|
||||
wakeObs = True
|
||||
_observer_wait_idle()
|
||||
|
||||
self.pruneLog("[test-phase 2) time+11m]")
|
||||
# jump to the future (+1 minute):
|
||||
_time_shift(1)
|
||||
# wait for observer idle (write all tickets to db):
|
||||
_observer_wait_idle()
|
||||
# wait for prolong:
|
||||
self.assertLogged(
|
||||
"stdout: '[test-jail1] test-action2: ++ prolong 192.0.2.11 -c 2 -t 600 : ",
|
||||
all=True, wait=MID_WAITTIME)
|
||||
|
||||
# get banned ips with time:
|
||||
_observer_wait_idle()
|
||||
self.pruneLog("[test-phase 2) time+11m - get-ips]")
|
||||
self.execCmd(SUCCESS, startparams, "get", "test-jail1", "banip", "--with-time")
|
||||
self.assertLogged(
|
||||
"192.0.2.11", "+ 600 =", all=True, wait=MID_WAITTIME)
|
||||
|
||||
# test stop with busy observer:
|
||||
self.pruneLog("[test-phase end) stop on busy observer]")
|
||||
tearDownMyTime()
|
||||
a = {'state': 0}
|
||||
obsMain = Observers.Main
|
||||
def _long_action():
|
||||
logSys.info('++ observer enters busy state ...')
|
||||
a['state'] = 1
|
||||
Utils.wait_for(lambda: a['state'] == 2, MAX_WAITTIME)
|
||||
obsMain.db_purge(); # does nothing (db is already None)
|
||||
logSys.info('-- observer leaves busy state.')
|
||||
obsMain.add('call', _long_action)
|
||||
obsMain.add('call', lambda: None)
|
||||
# wait observer enter busy state:
|
||||
Utils.wait_for(lambda: a['state'] == 1, MAX_WAITTIME)
|
||||
# overwrite default wait time (normally 5 seconds):
|
||||
obsMain_stop = obsMain.stop
|
||||
def _stop(wtime=(0.01 if unittest.F2B.fast else 0.1), forceQuit=True):
|
||||
return obsMain_stop(wtime, forceQuit)
|
||||
obsMain.stop = _stop
|
||||
# stop server and wait for end:
|
||||
self.stopAndWaitForServerEnd(SUCCESS)
|
||||
# check observer and db state:
|
||||
self.assertNotLogged('observer leaves busy state')
|
||||
self.assertFalse(obsMain.idle)
|
||||
self.assertEqual(obsMain._ObserverThread__db, None)
|
||||
# server is exited without wait for observer, stop it now:
|
||||
a['state'] = 2
|
||||
self.assertLogged('observer leaves busy state', wait=True)
|
||||
obsMain.join()
|
||||
|
||||
# test multiple start/stop of the server (threaded in foreground) --
|
||||
if False: # pragma: no cover
|
||||
@with_foreground_server_thread()
|
||||
@@ -1474,3 +1666,4 @@ class Fail2banServerTest(Fail2banClientServerBase):
|
||||
def testServerStartStop(self):
|
||||
for i in xrange(2000):
|
||||
self._testServerStartStop()
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ __license__ = "GPL"
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from ..client import fail2banregex
|
||||
@@ -80,6 +81,11 @@ def _test_exec_command_line(*args):
|
||||
sys.stderr = _org['stderr']
|
||||
return _exit_code
|
||||
|
||||
def _reset():
|
||||
# reset global warn-counter:
|
||||
from ..server.filter import _decode_line_warn
|
||||
_decode_line_warn.clear()
|
||||
|
||||
STR_00 = "Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 192.0.2.0"
|
||||
STR_00_NODT = "[sshd] error: PAM: Authentication failure for kevin from 192.0.2.0"
|
||||
|
||||
@@ -122,6 +128,7 @@ class Fail2banRegexTest(LogCaptureTestCase):
|
||||
"""Call before every test case."""
|
||||
LogCaptureTestCase.setUp(self)
|
||||
setUpMyTime()
|
||||
_reset()
|
||||
|
||||
def tearDown(self):
|
||||
"""Call after every test case."""
|
||||
@@ -141,6 +148,12 @@ class Fail2banRegexTest(LogCaptureTestCase):
|
||||
))
|
||||
self.assertLogged("Unable to compile regular expression")
|
||||
|
||||
def testWrongFilterOptions(self):
|
||||
self.assertFalse(_test_exec(
|
||||
"test", "flt[a='x,y,z',b=z,y,x]"
|
||||
))
|
||||
self.assertLogged("Wrong filter name or options", "wrong syntax at 14: y,x", all=True)
|
||||
|
||||
def testDirectFound(self):
|
||||
self.assertTrue(_test_exec(
|
||||
"--datepattern", r"^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
|
||||
@@ -341,6 +354,23 @@ class Fail2banRegexTest(LogCaptureTestCase):
|
||||
self.assertTrue(_test_exec('-o', 'id', STR_00, RE_00_ID))
|
||||
self.assertLogged('kevin')
|
||||
self.pruneLog()
|
||||
# multiple id combined to a tuple (id, tuple_id):
|
||||
self.assertTrue(_test_exec('-o', 'id',
|
||||
'1591983743.667 192.0.2.1 192.0.2.2',
|
||||
r'^\s*<F-ID/> <F-TUPLE_ID>\S+</F-TUPLE_ID>'))
|
||||
self.assertLogged(str(('192.0.2.1', '192.0.2.2')))
|
||||
self.pruneLog()
|
||||
# multiple id combined to a tuple, id first - (id, tuple_id_1, tuple_id_2):
|
||||
self.assertTrue(_test_exec('-o', 'id',
|
||||
'1591983743.667 left 192.0.2.3 right',
|
||||
r'^\s*<F-TUPLE_ID_1>\S+</F-TUPLE_ID_1> <F-ID/> <F-TUPLE_ID_2>\S+</F-TUPLE_ID_2>'))
|
||||
self.pruneLog()
|
||||
# id had higher precedence as ip-address:
|
||||
self.assertTrue(_test_exec('-o', 'id',
|
||||
'1591983743.667 left [192.0.2.4]:12345 right',
|
||||
r'^\s*<F-TUPLE_ID_1>\S+</F-TUPLE_ID_1> <F-ID><ADDR>:<F-PORT/></F-ID> <F-TUPLE_ID_2>\S+</F-TUPLE_ID_2>'))
|
||||
self.assertLogged(str(('[192.0.2.4]:12345', 'left', 'right')))
|
||||
self.pruneLog()
|
||||
# row with id :
|
||||
self.assertTrue(_test_exec('-o', 'row', STR_00, RE_00_ID))
|
||||
self.assertLogged("['kevin'", "'ip4': '192.0.2.0'", "'fid': 'kevin'", all=True)
|
||||
@@ -378,7 +408,17 @@ class Fail2banRegexTest(LogCaptureTestCase):
|
||||
"Found a match but no valid date/time found",
|
||||
"Match without a timestamp:", all=True)
|
||||
|
||||
self.pruneLog()
|
||||
def testIncompleteDateTime(self):
|
||||
# datepattern in followed lines doesn't match previously known pattern + line is too short
|
||||
# (logging break-off, no flush, etc):
|
||||
self.assertTrue(_test_exec(
|
||||
'-o', 'Found-ADDR:<ip>',
|
||||
'192.0.2.1 - - [02/May/2021:18:40:55 +0100] "GET / HTTP/1.1" 302 328 "-" "Mozilla/5.0" "-"\n'
|
||||
'192.0.2.2 - - [02/May/2021:18:40:55 +0100\n'
|
||||
'192.0.2.3 - - [02/May/2021:18:40:55',
|
||||
'^<ADDR>'))
|
||||
self.assertLogged(
|
||||
"Found-ADDR:192.0.2.1", "Found-ADDR:192.0.2.2", "Found-ADDR:192.0.2.3", all=True)
|
||||
|
||||
def testFrmtOutputWrapML(self):
|
||||
unittest.F2B.SkipIfCfgMissing(stock=True)
|
||||
@@ -431,14 +471,8 @@ class Fail2banRegexTest(LogCaptureTestCase):
|
||||
FILENAME_ZZZ_GEN, FILENAME_ZZZ_GEN
|
||||
))
|
||||
|
||||
def _reset(self):
|
||||
# reset global warn-counter:
|
||||
from ..server.filter import _decode_line_warn
|
||||
_decode_line_warn.clear()
|
||||
|
||||
def testWronChar(self):
|
||||
unittest.F2B.SkipIfCfgMissing(stock=True)
|
||||
self._reset()
|
||||
self.assertTrue(_test_exec(
|
||||
"-l", "notice", # put down log-level, because of too many debug-messages
|
||||
"--datepattern", r"^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
|
||||
@@ -454,7 +488,6 @@ class Fail2banRegexTest(LogCaptureTestCase):
|
||||
|
||||
def testWronCharDebuggex(self):
|
||||
unittest.F2B.SkipIfCfgMissing(stock=True)
|
||||
self._reset()
|
||||
self.assertTrue(_test_exec(
|
||||
"-l", "notice", # put down log-level, because of too many debug-messages
|
||||
"--datepattern", r"^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
|
||||
@@ -467,6 +500,36 @@ class Fail2banRegexTest(LogCaptureTestCase):
|
||||
|
||||
self.assertLogged('https://')
|
||||
|
||||
def testNLCharAsPartOfUniChar(self):
|
||||
fname = tempfile.mktemp(prefix='tmp_fail2ban', suffix='uni')
|
||||
# test two multi-byte encodings (both contains `\x0A` in either \x02\x0A or \x0A\x02):
|
||||
for enc in ('utf-16be', 'utf-16le'):
|
||||
self.pruneLog("[test-phase encoding=%s]" % enc)
|
||||
try:
|
||||
fout = open(fname, 'wb')
|
||||
# test on unicode string containing \x0A as part of uni-char,
|
||||
# it must produce exactly 2 lines (both are failures):
|
||||
for l in (
|
||||
u'1490349000 \u20AC Failed auth: invalid user Test\u020A from 192.0.2.1\n',
|
||||
u'1490349000 \u20AC Failed auth: invalid user TestI from 192.0.2.2\n'
|
||||
):
|
||||
fout.write(l.encode(enc))
|
||||
fout.close()
|
||||
|
||||
self.assertTrue(_test_exec(
|
||||
"-l", "notice", # put down log-level, because of too many debug-messages
|
||||
"--encoding", enc,
|
||||
"--datepattern", r"^EPOCH",
|
||||
fname, r"Failed .* from <HOST>",
|
||||
))
|
||||
|
||||
self.assertLogged(" encoding : %s" % enc,
|
||||
"Lines: 2 lines, 0 ignored, 2 matched, 0 missed", all=True)
|
||||
self.assertNotLogged("Missed line(s)")
|
||||
finally:
|
||||
fout.close()
|
||||
os.unlink(fname)
|
||||
|
||||
def testExecCmdLine_Usage(self):
|
||||
self.assertNotEqual(_test_exec_command_line(), 0)
|
||||
self.pruneLog()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user