Révision 0341e680
[plugins/shorewall/shorewall_log] added plugin for monitoring shorewall blocks
Graphs the number of blocks done by shorewall, given a specific rule
suffix
| plugins/shorewall/shorewall_log | ||
|---|---|---|
| 1 |
#!/usr/bin/env python3 |
|
| 2 |
# -*- python -*- |
|
| 3 |
|
|
| 4 |
""" |
|
| 5 |
|
|
| 6 |
=head1 NAME |
|
| 7 |
|
|
| 8 |
Plugin to monitor iptables logs configured by shorewall |
|
| 9 |
|
|
| 10 |
=head1 CONFIGURATION |
|
| 11 |
|
|
| 12 |
logfile: Path to the iptables log file, or "journald" to use journald. |
|
| 13 |
When journalctl exists, the default is "journald", otherwise |
|
| 14 |
"/var/log/kern.log". |
|
| 15 |
journalctlargs: Arguments passed to journalctl to select the right logs. |
|
| 16 |
The default is "SYSLOG_IDENTIFIER=kernel". |
|
| 17 |
taggroups: Space separated list of groups. A group contains a tag if the |
|
| 18 |
group is substring of the tag. Tags belonging to the same group |
|
| 19 |
will be combined in one graph. |
|
| 20 |
tagfilter: Space separated list of filters. When a tag is matched by a |
|
| 21 |
filter (i.e. if the filter is a substring of the tag) it is |
|
| 22 |
ignored. |
|
| 23 |
prefixformat: The format of the prefix configured in iptables, this is the |
|
| 24 |
LOGFORMAT option in shorewall.conf. When not set the entire |
|
| 25 |
prefix is used. |
|
| 26 |
include_ungrouped: when a tag is found that does not belong to a group, |
|
| 27 |
make it it's own group |
|
| 28 |
|
|
| 29 |
Example: |
|
| 30 |
|
|
| 31 |
Using /var/log/kern.log as logfile: |
|
| 32 |
|
|
| 33 |
=over 2 |
|
| 34 |
|
|
| 35 |
[shorewall_log] |
|
| 36 |
group adm |
|
| 37 |
env.logfile /var/log/kern.log |
|
| 38 |
|
|
| 39 |
=back |
|
| 40 |
|
|
| 41 |
Using journald: |
|
| 42 |
|
|
| 43 |
=over 2 |
|
| 44 |
|
|
| 45 |
[shorewall_log] |
|
| 46 |
group systemd-journal |
|
| 47 |
|
|
| 48 |
=back |
|
| 49 |
|
|
| 50 |
=head1 HISTORY |
|
| 51 |
|
|
| 52 |
2017-11-03: v1.0 Bert Van de Poel <bert@bhack.net>: created |
|
| 53 |
2020-07-16: v2.0 Vincent Vanlaer <vincenttc@ulyssis.org>: rewrite |
|
| 54 |
- read all tags from iptables config, instead of the last 24h |
|
| 55 |
of logs |
|
| 56 |
- add support for journald |
|
| 57 |
- use cursors for accuracy |
|
| 58 |
- convert to multigraph to reduce load |
|
| 59 |
|
|
| 60 |
=head1 USAGE |
|
| 61 |
|
|
| 62 |
Parameters understood: |
|
| 63 |
|
|
| 64 |
config (required) |
|
| 65 |
autoconf (optional - used by munin-config) |
|
| 66 |
|
|
| 67 |
=head1 MAGIC MARKERS |
|
| 68 |
|
|
| 69 |
#%# family=auto |
|
| 70 |
#%# capabilities=autoconf |
|
| 71 |
|
|
| 72 |
=cut |
|
| 73 |
""" |
|
| 74 |
|
|
| 75 |
|
|
| 76 |
import sys |
|
| 77 |
import os |
|
| 78 |
|
|
| 79 |
|
|
| 80 |
def autoconf() -> str: |
|
| 81 |
if sys.version_info < (3, 5): |
|
| 82 |
return 'no (This plugin requires python 3.5 or higher)' |
|
| 83 |
|
|
| 84 |
if os.getenv('MUNIN_CAP_MULTIGRAPH', '0') != '1':
|
|
| 85 |
return 'no (No multigraph support)' |
|
| 86 |
|
|
| 87 |
import shutil |
|
| 88 |
|
|
| 89 |
if not shutil.which('shorewall'):
|
|
| 90 |
return 'no (No shorewall executable found)' |
|
| 91 |
|
|
| 92 |
if not shutil.which('iptables-save'):
|
|
| 93 |
return 'no (No iptables-save executable found, required for tag enumeration)' |
|
| 94 |
|
|
| 95 |
return 'yes' |
|
| 96 |
|
|
| 97 |
|
|
| 98 |
if len(sys.argv) == 2 and sys.argv[1] == "autoconf": |
|
| 99 |
print(autoconf()) |
|
| 100 |
sys.exit() |
|
| 101 |
|
|
| 102 |
|
|
| 103 |
from collections import defaultdict, namedtuple |
|
| 104 |
from typing import Set, Iterator, TextIO, Dict, Tuple |
|
| 105 |
from itertools import takewhile |
|
| 106 |
from subprocess import run, PIPE |
|
| 107 |
import shlex |
|
| 108 |
import shutil |
|
| 109 |
import pickle |
|
| 110 |
import re |
|
| 111 |
|
|
| 112 |
|
|
| 113 |
logfile = os.getenv('logfile', 'journald' if shutil.which('journalctl') else '/var/log/kern.log')
|
|
| 114 |
journalctl_args = list(shlex.split(os.getenv('journalctlargs',
|
|
| 115 |
'SYSLOG_IDENTIFIER=kernel'))) |
|
| 116 |
taggroups = os.getenv('taggroups')
|
|
| 117 |
taggroups = taggroups.split() if taggroups else [] |
|
| 118 |
tagfilter = os.getenv('tagfilter')
|
|
| 119 |
tagfilter = tagfilter.split() if tagfilter else [] |
|
| 120 |
include_ungrouped = (os.getenv('includeungrouped', 'true').lower() == 'true')
|
|
| 121 |
prefix_format = os.getenv('prefixformat')
|
|
| 122 |
if prefix_format: |
|
| 123 |
if sys.version_info < (3, 7): |
|
| 124 |
prefix_format = re.escape(prefix_format).replace('\\%s', '(.+)').replace('\\%d', '\\d+')
|
|
| 125 |
else: # % is no longer escaped |
|
| 126 |
prefix_format = re.escape(prefix_format).replace('%s', '(.+)').replace('%d', '\\d+')
|
|
| 127 |
prefix_format = re.compile(prefix_format) |
|
| 128 |
|
|
| 129 |
|
|
| 130 |
def get_logtags() -> Tuple[Dict[str, Set[str]], Dict[str, Set[str]]]: |
|
| 131 |
rules = (run(['iptables-save'], stdout=PIPE, universal_newlines=True). |
|
| 132 |
stdout.splitlines()) |
|
| 133 |
tags = defaultdict(set) |
|
| 134 |
groups = defaultdict(set) |
|
| 135 |
|
|
| 136 |
# every line is an iptables rule, in the iptables command/args syntax |
|
| 137 |
# (without the 'iptables' command listed), eg. |
|
| 138 |
# "-A INPUT -p tcp -s 10.3.3.7 -j DROP" |
|
| 139 |
for line in rules: |
|
| 140 |
args = iter(shlex.split(line)) |
|
| 141 |
for arg in args: |
|
| 142 |
# we only want rules that log packets, not that accept/drop/... |
|
| 143 |
if arg == '-j' and next(args) != 'LOG': |
|
| 144 |
break |
|
| 145 |
# and we only need to know the logging tag, and add it to the list |
|
| 146 |
if arg == '--log-prefix': |
|
| 147 |
prefix = next(args) |
|
| 148 |
|
|
| 149 |
if prefix_format: |
|
| 150 |
tag = prefix_format.match(prefix)[1] |
|
| 151 |
else: |
|
| 152 |
tag = prefix.rstrip() |
|
| 153 |
|
|
| 154 |
if any(ignored in tag for ignored in tagfilter): |
|
| 155 |
continue |
|
| 156 |
tags[tag].add(prefix) |
|
| 157 |
|
|
| 158 |
for group in taggroups: |
|
| 159 |
if group in tag: |
|
| 160 |
groups[group].add(tag) |
|
| 161 |
break |
|
| 162 |
else: |
|
| 163 |
if include_ungrouped: |
|
| 164 |
groups[tag].add(tag) |
|
| 165 |
|
|
| 166 |
break |
|
| 167 |
|
|
| 168 |
return groups, tags |
|
| 169 |
|
|
| 170 |
|
|
| 171 |
State = namedtuple('State', ['journal', 'file'])
|
|
| 172 |
|
|
| 173 |
|
|
| 174 |
def load_state() -> State: |
|
| 175 |
try: |
|
| 176 |
with open(os.getenv('MUNIN_STATEFILE'), 'rb') as f:
|
|
| 177 |
return pickle.load(f) |
|
| 178 |
except OSError: |
|
| 179 |
return State(None, None) |
|
| 180 |
|
|
| 181 |
|
|
| 182 |
def save_state(state: State): |
|
| 183 |
with open(os.getenv('MUNIN_STATEFILE'), 'wb') as f:
|
|
| 184 |
return pickle.dump(state, f) |
|
| 185 |
|
|
| 186 |
|
|
| 187 |
def get_lines_journalctl(state: State) -> Iterator[str]: |
|
| 188 |
cursor = state.journal |
|
| 189 |
|
|
| 190 |
def catch_cursor(line: str): |
|
| 191 |
cursor_id = '-- cursor: ' |
|
| 192 |
if line.startswith(cursor_id): |
|
| 193 |
save_state(State(line[len(cursor_id):], None)) |
|
| 194 |
return False |
|
| 195 |
else: |
|
| 196 |
return True |
|
| 197 |
|
|
| 198 |
if not cursor: # prevent reading the entire journal on first run |
|
| 199 |
journal = run(['journalctl', '--no-pager', '--quiet', '--lines=0', |
|
| 200 |
'--show-cursor', *journalctl_args], |
|
| 201 |
stdout=PIPE, universal_newlines=True) |
|
| 202 |
else: |
|
| 203 |
journal = run(['journalctl', '--no-pager', '--quiet', '--show-cursor', |
|
| 204 |
'--after-cursor', cursor, *journalctl_args], |
|
| 205 |
stdout=PIPE, universal_newlines=True) |
|
| 206 |
|
|
| 207 |
yield from filter(catch_cursor, journal.stdout.splitlines()) |
|
| 208 |
|
|
| 209 |
|
|
| 210 |
def reverse_read(f: TextIO) -> Iterator[str]: |
|
| 211 |
BUFSIZE = 4096 |
|
| 212 |
f.seek(0, 2) |
|
| 213 |
position = f.tell() |
|
| 214 |
remainder = '' |
|
| 215 |
while position > 0: |
|
| 216 |
position = max(position - BUFSIZE, 0) |
|
| 217 |
f.seek(position) |
|
| 218 |
lines = f.read(BUFSIZE).splitlines() |
|
| 219 |
lines[-1] += remainder |
|
| 220 |
remainder = lines.pop(0) |
|
| 221 |
yield from reversed(lines) |
|
| 222 |
yield remainder |
|
| 223 |
|
|
| 224 |
|
|
| 225 |
def get_lines_logfile(path: str, state: State) -> Iterator[str]: |
|
| 226 |
with open(path, 'r') as f: |
|
| 227 |
cursor = state.file |
|
| 228 |
|
|
| 229 |
reader = reverse_read(f) |
|
| 230 |
|
|
| 231 |
if not cursor: |
|
| 232 |
save_state(State(None, next(reader))) |
|
| 233 |
return |
|
| 234 |
else: |
|
| 235 |
new_cursor = next(reader) |
|
| 236 |
save_state(State(None, new_cursor)) |
|
| 237 |
yield new_cursor |
|
| 238 |
yield from takewhile(lambda x: x != cursor, reader) |
|
| 239 |
|
|
| 240 |
|
|
| 241 |
def get_tagcount(tags: Dict[str, Set[str]]) -> Dict[str, int]: |
|
| 242 |
count = defaultdict(int) |
|
| 243 |
state = load_state() |
|
| 244 |
|
|
| 245 |
if logfile == 'journald': |
|
| 246 |
lines = get_lines_journalctl(state) |
|
| 247 |
offset = 5 |
|
| 248 |
else: |
|
| 249 |
lines = get_lines_logfile(logfile, state) |
|
| 250 |
offset = 6 |
|
| 251 |
|
|
| 252 |
for line in lines: |
|
| 253 |
if 'IN=' not in line: |
|
| 254 |
continue |
|
| 255 |
line = line.replace(' ', ' ').split(' ', maxsplit=offset)[-1]
|
|
| 256 |
|
|
| 257 |
for tag, prefixes in tags.items(): |
|
| 258 |
for prefix in prefixes: |
|
| 259 |
if line.startswith(prefix): |
|
| 260 |
count[tag] += 1 |
|
| 261 |
break |
|
| 262 |
else: |
|
| 263 |
continue |
|
| 264 |
break |
|
| 265 |
|
|
| 266 |
return count |
|
| 267 |
|
|
| 268 |
|
|
| 269 |
def fetch(): |
|
| 270 |
groups, logtags = get_logtags() |
|
| 271 |
tagcount = get_tagcount(logtags) |
|
| 272 |
|
|
| 273 |
for group, tags in groups.items(): |
|
| 274 |
print('multigraph shorewall_{}'.format(group))
|
|
| 275 |
for tag in tags: |
|
| 276 |
print('{}.value {}'.format(tag.lower(), tagcount[tag]))
|
|
| 277 |
|
|
| 278 |
|
|
| 279 |
def config(): |
|
| 280 |
|
|
| 281 |
for group, tags in get_logtags()[0].items(): |
|
| 282 |
print('multigraph shorewall_{}'.format(group))
|
|
| 283 |
print('graph_title Shorewall Logs for {}'.format(group))
|
|
| 284 |
print('graph_vlabel entries per ${graph_period}')
|
|
| 285 |
print('graph_category shorewall')
|
|
| 286 |
|
|
| 287 |
for tag in sorted(tags): |
|
| 288 |
print('{}.label {}'.format(tag.lower(), tag))
|
|
| 289 |
print('{}.type ABSOLUTE'.format(tag.lower()))
|
|
| 290 |
print('{}.draw AREASTACK'.format(tag.lower()))
|
|
| 291 |
|
|
| 292 |
|
|
| 293 |
if len(sys.argv) == 2 and sys.argv[1] == "config": |
|
| 294 |
config() |
|
| 295 |
else: |
|
| 296 |
fetch() |
|
| 297 |
|
|
| 298 |
# flake8: noqa: E265,E402 |
|
Formats disponibles : Unified diff