├── .gitignore ├── COPYING ├── README.md ├── nfct_cffi.py ├── nfct_logger.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | /*.egg-info 2 | /build 3 | /dist 4 | /README.txt 5 | /MANIFEST 6 | /__pycache__ 7 | *.pyc 8 | *.pyo 9 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2012 Mike Kazantsev 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | conntrack-logger 2 | -------------------- 3 | 4 | Tool to make best effort to log conntrack flows along with associated pids, 5 | which service cgroup they belong to and misc other info about them. 6 | 7 | Think of it as an auditd extension to log network connections. 8 | 9 | Main purpose is to keep track of what (if anything) in the system tries to 10 | establish fishy or unauthorized connections. 11 | 12 | For example, imagine your IDS spots a occasional (e.g. 1 per day/week) 13 | connections to known botnet hosts from one of the intranet machines. 14 | You get a dump of some encrypted traffic that gets passed, but looking at the 15 | machine in question, you've no idea which pid initiated these at the time - only 16 | clue is transient port numbers, which are useful only while connection lasts. 17 | 18 | This tool allows to attribute such logged connections to pids (which might be 19 | e.g. forked curl, hence not useful by itself) and services they belong to, 20 | assuming proper service-pid-tracking system (i.e. systemd cgroups) is in place. 21 | 22 | Unlike e.g. [netstat-monitor](https://github.com/stalexan/netstat-monitor/), it 23 | doesn't poll `/proc/net/*` paths (though still uses them to map flow back to 24 | pid), getting "new flow" events via libnetfilter_conntrack (netlink socket) 25 | instead, in a bit more efficient manner. 26 | 27 | 28 | 29 | Usage 30 | -------------------- 31 | 32 | Just run nfct_logger.py and get the entries from its stdout (lines wrapped for readability): 33 | 34 | ```console 35 | # ./nfct_logger.py -p tcp 36 | 1373127181: ipv6/tcp 2001:470:1f0b:11de::12/55446 > 2607:f8b0:4006:802::1010/80 :: 37 | 2354 1000:1000 /user/1000.user/1.session/systemd-1196/enlightenment.service :: 38 | curl -s -o /dev/null ipv6.google.com 39 | 1373127199: ipv4/tcp 192.168.0.12/34870 > 195.24.232.208/80 :: 40 | 28865 1000:1000 /user/1000.user/1.session/systemd-1196/dbus.service :: 41 | python /usr/libexec/dbus-lastfm-scrobbler 42 | 1373127220: ipv4/tcp 127.0.0.1/59047 > 127.0.0.1/1234 :: 43 | 2387 1000:1000 /user/1000.user/1.session/systemd-1196/enlightenment.service :: 44 | ncat -v cane 1234 45 | ``` 46 | 47 | Default log format (can be controlled via --format, timestamp format via --format-ts) is (wrapped): 48 | 49 | {ts}: {ev.proto} {ev.src}/{ev.sport} > {ev.dst}/{ev.dport} :: 50 | {info.pid} {info.uid}:{info.gid} {info.service} :: {info.cmdline} 51 | 52 | Info about pid might not be available for transient connections, like one-way 53 | udp packets, as these don't seem to end up in /proc/net/udp (or udp6) tables, 54 | hence it's hard to get socket "inode" number. 55 | 56 | As netfilter, conntrack and netlink sockets are linux-specific things (afaik), 57 | script should not work on any other platforms, unless there is some 58 | compatibility layer in place. 59 | 60 | 61 | ### nfct_cffi 62 | 63 | Tool is based on bundled nfct_cffi module, which can be used from any python 64 | code: 65 | 66 | ```python 67 | from nfct_cffi import NFCT 68 | 69 | src = NFCT().generator() 70 | print 'Netlink fd: {} (to e.g. integrate into eventloop)'.format(next(src)) 71 | for data in src: 72 | print 'Got event: {}'.format(data) 73 | ``` 74 | 75 | Module uses libnetfilter_conntrack via CFFI. 76 | 77 | 78 | 79 | Installation 80 | -------------------- 81 | 82 | It's a regular package for Python 2.7 (not 3.X), but not in pypi, so can be 83 | installed from a checkout with something like that: 84 | 85 | % python setup.py install 86 | 87 | Better way would be to use [pip](http://pip-installer.org/) to install all the 88 | necessary dependencies as well: 89 | 90 | % pip install 'git+https://github.com/mk-fg/conntrack-logger.git#egg=conntrack-logger' 91 | 92 | Note that to install stuff in system-wide PATH and site-packages, elevated 93 | privileges are often required. 94 | Use "install --user", 95 | [~/.pydistutils.cfg](http://docs.python.org/install/index.html#distutils-configuration-files) 96 | or [virtualenv](http://pypi.python.org/pypi/virtualenv) to do unprivileged 97 | installs into custom paths. 98 | 99 | Alternatively, `./nfct_logger.py` can be run right from the checkout tree 100 | without any installation. 101 | 102 | ### Requirements 103 | 104 | * Python 2.7 (not 3.X) 105 | * [CFFI](http://cffi.readthedocs.org) (for libnetfilter_conntrack bindings) 106 | * [libnetfilter_conntrack](http://www.netfilter.org/projects/libnetfilter_conntrack) 107 | * nf_conntrack_netlink kernel module (e.g. `modprobe nf_conntrack_netlink`) 108 | 109 | CFFI uses C compiler to generate bindings, so gcc (or other compiler) should be 110 | available if module is being built from source or used from checkout tree. 111 | 112 | To install these requirements on Debian/Ubuntu (tested on Ubuntu "Xenial" 113 | 16.04), use: 114 | 115 | # apt install build-essential libnfnetlink-dev python-cffi libnetfilter-conntrack-dev libpython2.7-dev 116 | 117 | 118 | Limitations 119 | -------------------- 120 | 121 | When new flow event is received from libnetfilter_conntrack, it 122 | [doesn't have "pid" attribute](https://git.netfilter.org/libnetfilter_conntrack/tree/include/libnetfilter_conntrack/libnetfilter_conntrack.h#n62) 123 | associated with it, so script looks up corresponding line in `/proc/net/*` to 124 | pick "inode" number for connection from there, then does 125 | `glob('/proc/[0-9]*/fd/[0-9]*')`, readlink() on each to find which one leads to 126 | socket matching that inode and then grabs/prints info for the pid from there on 127 | match. 128 | 129 | So for super-quick connections, slow pid context switching, lots of pids or 130 | something, it might fail to match socket/pid in time, while both are still 131 | around, printing only connection info instead. 132 | 133 | Running curl on even the fastest url probably won't ever slip by the logging, 134 | but some fast app opening socket, sending a packet, then closing it immediately 135 | afterwards can do that. 136 | 137 | [auditd](https://people.redhat.com/sgrubb/audit) is probably a tool to track 138 | such things in a more dedicated way. 139 | -------------------------------------------------------------------------------- /nfct_cffi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | #-*- coding: utf-8 -*- 3 | from __future__ import print_function 4 | 5 | import itertools as it, operator as op, functools as ft 6 | import os, sys, signal 7 | 8 | from cffi import FFI 9 | 10 | 11 | # Try to work around insane "write_table" operations (which assume that 12 | # they can just write lextab.py and yacctab.py in current dir), used by default. 13 | try: from ply.lex import Lexer 14 | except ImportError: pass 15 | else: Lexer.writetab = lambda s,*a,**k: None 16 | try: from ply.yacc import LRGeneratedTable 17 | except ImportError: pass 18 | else: LRGeneratedTable.write_table = lambda s,*a,**k: None 19 | 20 | 21 | # There're no defs for conntrack-expectations' handling here 22 | # Also nfct_nfnlh() can be useful here for e.g. nfnl_rcvbufsiz() 23 | _cdef = ''' 24 | typedef unsigned char u_int8_t; 25 | typedef unsigned short int u_int16_t; 26 | typedef unsigned int u_int32_t; 27 | 28 | static const u_int8_t NFNL_SUBSYS_NONE; 29 | static const u_int8_t NFNL_SUBSYS_CTNETLINK; 30 | 31 | static const unsigned int NFNLGRP_NONE; 32 | static const unsigned int NFNLGRP_CONNTRACK_NEW; 33 | static const unsigned int NFNLGRP_CONNTRACK_UPDATE; 34 | static const unsigned int NFNLGRP_CONNTRACK_DESTROY; 35 | 36 | enum nf_conntrack_msg_type { 37 | NFCT_T_UNKNOWN, 38 | NFCT_T_NEW, 39 | NFCT_T_UPDATE, 40 | NFCT_T_DESTROY, 41 | NFCT_T_ALL, 42 | NFCT_T_ERROR, 43 | ... 44 | }; 45 | 46 | enum nfct_cb { 47 | NFCT_CB_FAILURE, 48 | NFCT_CB_STOP, 49 | NFCT_CB_CONTINUE, 50 | NFCT_CB_STOLEN, 51 | ... 52 | }; 53 | 54 | enum nfct_o { 55 | NFCT_O_PLAIN, 56 | NFCT_O_DEFAULT, 57 | NFCT_O_XML, 58 | NFCT_O_MAX, 59 | ... 60 | }; 61 | 62 | enum nfct_of { 63 | NFCT_OF_SHOW_LAYER3, 64 | NFCT_OF_TIME, 65 | NFCT_OF_ID, 66 | NFCT_OF_TIMESTAMP, 67 | ... 68 | }; 69 | 70 | struct nfct_handle* nfct_open(u_int8_t subsys_id, unsigned int subscriptions); 71 | int nfct_close(struct nfct_handle * cth); 72 | int nfct_fd(struct nfct_handle *cth); 73 | 74 | struct nlmsghdr { 75 | u_int32_t nlmsg_len; /* Length of message including header */ 76 | u_int16_t nlmsg_type; /* Message content */ 77 | u_int16_t nlmsg_flags; /* Additional flags */ 78 | u_int32_t nlmsg_seq; /* Sequence number */ 79 | u_int32_t nlmsg_pid; /* Sending process port ID */ 80 | }; 81 | 82 | typedef int nfct_callback( 83 | const struct nlmsghdr *nlh, 84 | enum nf_conntrack_msg_type type, 85 | struct nf_conntrack *ct, void *data ); 86 | 87 | int nfct_callback_register2( 88 | struct nfct_handle *h, 89 | enum nf_conntrack_msg_type type, 90 | nfct_callback *cb, void *data ); 91 | 92 | void nfct_callback_unregister2(struct nfct_handle *h); 93 | 94 | int nfct_catch(struct nfct_handle *h); 95 | 96 | int nfct_snprintf( 97 | char *buf, 98 | unsigned int size, 99 | const struct nf_conntrack *ct, 100 | const unsigned int msg_type, 101 | const unsigned int out_type, 102 | const unsigned int out_flags ); 103 | ''' 104 | 105 | _clibs_includes = ''' 106 | #include 107 | #include 108 | #include 109 | ''' 110 | _clibs_link = 'nfnetlink', 'netfilter_conntrack' 111 | 112 | 113 | class NFCTError(OSError): pass 114 | 115 | NFWouldBlock = type('NFWouldBlock', (object,), dict()) 116 | 117 | 118 | class NFCT(object): 119 | 120 | _instance = None 121 | 122 | def __new__(cls): 123 | if not cls._instance: 124 | cls._instance = super(NFCT, cls).__new__(cls) 125 | return cls._instance 126 | 127 | def __init__(self): 128 | global _cdef, _clibs_includes, _clibs_link 129 | self.ffi = FFI() 130 | self.ffi.cdef(_cdef) 131 | self.libnfct = self.ffi.verify(_clibs_includes, libraries=list(_clibs_link)) 132 | self.libnfct_cache = dict() 133 | _cdef = _clibs_includes = _clibs_link = None 134 | 135 | 136 | def _ffi_call( self, func, args, 137 | no_check=False, check_gt0=False, check_notnull=False ): 138 | '''Call lib function through cffi, 139 | checking return value and raising error, if necessary. 140 | Checks if return is >0 by default.''' 141 | res = func(*args) 142 | if no_check\ 143 | or (check_gt0 and res > 0)\ 144 | or (check_notnull and res)\ 145 | or res >= 0: return res 146 | errno_ = self.ffi.errno 147 | raise NFCTError(errno_, os.strerror(errno_)) 148 | 149 | def __getattr__(self, k): 150 | if not (k.startswith('nfct_') or k.startswith('c_')): 151 | return super(NFCT, self).__getattr__(k) 152 | if k.startswith('c_'): k = k[2:] 153 | if k not in self.libnfct_cache: 154 | func = getattr(self.libnfct, k) 155 | self.libnfct_cache[k] = lambda *a,**kw: self._ffi_call(func, a, **kw) 156 | return self.libnfct_cache[k] 157 | 158 | 159 | def generator(self, events=None, output_flags=None, handle_sigint=True): 160 | '''Generator that yields: 161 | - on first iteration - netlink fd that can be poll'ed 162 | or integrated into some event loop (twisted, gevent, ...). 163 | Also, that is the point where uid/gid/caps can be dropped. 164 | - on all subsequent iterations it does recv() on that fd, 165 | yielding XML representation of the captured conntrack event. 166 | Keywords: 167 | events: mask for event types to capture 168 | - or'ed NFNLGRP_CONNTRACK_* flags, None = all. 169 | output_flags: which info will be in resulting xml 170 | - or'ed NFCT_OF_* flags, None = set all. 171 | handle_sigint: add SIGINT handler to process it gracefully.''' 172 | 173 | if events is None: 174 | events = ( 175 | self.libnfct.NFNLGRP_CONNTRACK_NEW | 176 | self.libnfct.NFNLGRP_CONNTRACK_UPDATE | 177 | self.libnfct.NFNLGRP_CONNTRACK_DESTROY ) 178 | if output_flags is None: 179 | output_flags = ( 180 | self.libnfct.NFCT_OF_TIME | 181 | self.libnfct.NFCT_OF_ID | 182 | self.libnfct.NFCT_OF_SHOW_LAYER3 | 183 | self.libnfct.NFCT_OF_TIMESTAMP ) 184 | 185 | handle = self.nfct_open( 186 | self.libnfct.NFNL_SUBSYS_NONE, events, check_notnull=True ) 187 | 188 | cb_results = list() 189 | xml_buff_size = 2048 # ipv6 events are ~1k 190 | xml_buff = self.ffi.new('char[]', xml_buff_size) 191 | 192 | @self.ffi.callback('nfct_callback') 193 | def recv_callback(handler, msg_type, ct_struct, data): 194 | try: 195 | size = self.nfct_snprintf( xml_buff, xml_buff_size, ct_struct, 196 | msg_type, self.libnfct.NFCT_O_XML, output_flags, check_gt0=True ) 197 | assert size <= xml_buff_size, size # make sure xml fits 198 | data = self.ffi.buffer(xml_buff, size)[:] 199 | cb_results.append(data) 200 | except: 201 | cb_results.append(StopIteration) # breaks the generator 202 | raise 203 | return self.libnfct.NFCT_CB_STOP # to yield processed data from generator 204 | 205 | if handle_sigint: 206 | global _sigint_raise 207 | _sigint_raise = False 208 | def sigint_handler(sig, frm): 209 | global _sigint_raise 210 | _sigint_raise = True 211 | cb_results.append(StopIteration) 212 | sigint_handler = signal.signal(signal.SIGINT, sigint_handler) 213 | 214 | def break_check(val): 215 | if val is StopIteration: raise val() 216 | return val 217 | 218 | self.nfct_callback_register2( 219 | handle, self.libnfct.NFCT_T_ALL, recv_callback, self.ffi.NULL ) 220 | try: 221 | peek = break_check((yield self.nfct_fd(handle))) # yield fd for poll() on first iteration 222 | while True: 223 | if peek: 224 | peek = break_check((yield NFWouldBlock)) # poll/recv is required 225 | continue 226 | # No idea how many times callback will be used here 227 | self.nfct_catch(handle) 228 | if handle_sigint and _sigint_raise: raise KeyboardInterrupt() 229 | # Yield individual events 230 | for result in cb_results: 231 | break_check(result) 232 | peek = break_check((yield result)) 233 | cb_results = list() 234 | 235 | finally: 236 | if handle_sigint: signal.signal(signal.SIGINT, sigint_handler) 237 | self.nfct_callback_unregister2(handle, no_check=True) 238 | self.nfct_close(handle) 239 | 240 | 241 | if __name__ == '__main__': 242 | src = NFCT().generator() 243 | print('Netlink fd: {}, started logging conntrack events'.format(next(src))) 244 | for data in src: 245 | print('Event: {}'.format(data)) 246 | -------------------------------------------------------------------------------- /nfct_logger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | #-*- coding: utf-8 -*- 3 | from __future__ import print_function 4 | 5 | import itertools as it, operator as op, functools as ft 6 | from xml.etree import ElementTree 7 | from io import BytesIO 8 | from datetime import datetime 9 | from collections import namedtuple, defaultdict 10 | import os, sys, logging, re, glob, errno, socket 11 | 12 | from nfct_cffi import NFCT 13 | 14 | 15 | FlowData = namedtuple('FlowData', 'ts proto src dst sport dport') 16 | 17 | def parse_event(ev_xml): 18 | etree = ElementTree.parse(BytesIO(ev_xml)) 19 | 20 | flow = next(etree.iter()) 21 | assert flow.attrib['type'] == 'new', ev_xml 22 | 23 | ts = flow.find('when') 24 | ts = datetime(*(int(ts.find(k).text) for k in ['year', 'month', 'day', 'hour', 'min', 'sec'])) 25 | 26 | flow_data = dict() 27 | for meta in flow.findall('meta'): 28 | if meta.attrib['direction'] in ['original']: 29 | l3, l4 = it.imap(meta.find, ['layer3', 'layer4']) 30 | proto = l3.attrib['protoname'], l4.attrib['protoname'] 31 | if proto[1] not in ['tcp', 'udp']: return 32 | proto = '{}/{}'.format(*proto) 33 | src, dst = (l3.find(k).text for k in ['src', 'dst']) 34 | sport, dport = (int(l4.find(k).text) for k in ['sport', 'dport']) 35 | flow_data[meta.attrib['direction']] = FlowData(ts, proto, src, dst, sport, dport) 36 | 37 | ## Fairly sure tcp table should contain stuff from original flow, 38 | ## otherwise check here should probably pick direction somehow 39 | # fo, fr = op.itemgetter('original', 'reply')(flow_data) 40 | # assert fo.proto == fr.proto\ 41 | # and fo.src == fr.dst and fo.dst == fr.src\ 42 | # and fo.sport == fr.dport and fo.dport == fr.sport,\ 43 | # flow_data 44 | 45 | return flow_data['original'] 46 | 47 | 48 | def parse_ipv4(enc): 49 | return socket.inet_ntop(socket.AF_INET, ''.join(reversed(enc.decode('hex')))) 50 | 51 | def parse_ipv6( enc, 52 | _endian=op.itemgetter(*(slice(n*2, (n+1)*2) for n in [6, 7, 4, 5, 2, 3, 0, 1])) ): 53 | return socket.inet_ntop( socket.AF_INET6, 54 | ''.join(_endian(''.join(reversed(enc.decode('hex'))))) ) 55 | 56 | def get_table_sk( proto, 57 | _line_proc=op.itemgetter(1, 2, 9), 58 | _proto_conns={ 59 | 'ipv4/tcp': '/proc/net/tcp', 'ipv6/tcp': '/proc/net/tcp6', 60 | 'ipv4/udp': '/proc/net/udp', 'ipv6/udp': '/proc/net/udp6' } ): 61 | _ntoa = ft.partial(parse_ipv4 if proto.startswith('ipv4/') else parse_ipv6) 62 | with open(_proto_conns[proto]) as src: 63 | next(src) 64 | for line in src: 65 | a, b, sk = _line_proc(line.split()) 66 | k = (ep.split(':', 1) for ep in [a, b]) 67 | k = tuple(sorted((_ntoa(ip), int(p, 16)) for ip,p in k)) 68 | yield k, sk 69 | 70 | 71 | def get_table_links(): 72 | links = list() 73 | for path in glob.iglob('/proc/[0-9]*/fd/[0-9]*'): 74 | try: link = os.readlink(path) 75 | except (OSError, IOError) as err: 76 | if err.errno != errno.ENOENT: raise 77 | continue 78 | links.append((path, link)) 79 | for path, link in links: 80 | match = re.search(r'^socket:\[([^]]+)\]$', link) 81 | if not match: continue 82 | yield match.group(1), int(re.search(r'^/proc/(\d+)/', path).group(1)) 83 | 84 | 85 | class ProcReadFailure(Exception): 86 | errno = 0 87 | def __init__(self, err): 88 | if isinstance(err, (OSError, IOError)): 89 | self.errno = err.errno 90 | err = err.message 91 | super(ProcReadFailure, self).__init__(err) 92 | 93 | def proc_get(path): 94 | try: return open(path).read() 95 | except (OSError, IOError) as err: 96 | # errno.ESRCH is "IOError: [Errno 3] No such process" 97 | if err.errno not in [errno.ENOENT, errno.ESRCH]: raise 98 | raise ProcReadFailure(err) 99 | 100 | def pid_info(pid, entry): 101 | return proc_get('/proc/{}/{}'.format(pid, entry)) 102 | 103 | class FlowInfo(namedtuple('FlowInfo', 'pid uid gid cmdline service')): 104 | __slots__ = tuple() 105 | 106 | def __new__(cls, pid=None): 107 | uid = gid = cmdline = service = '-' 108 | if pid is not None: 109 | try: 110 | cmdline, service = (pid_info(pid, k) for k in ['cmdline', 'cgroup']) 111 | stat = os.stat('/proc/{}'.format(pid)) 112 | uid, gid = op.attrgetter('st_uid', 'st_gid')(stat) 113 | except (OSError, IOError, ProcReadFailure) as err: 114 | if err.errno != errno.ENOENT: raise 115 | if cmdline != '-': cmdline = cmdline.replace('\0', ' ').strip() 116 | if service != '-': 117 | for line in service.splitlines(): 118 | line = line.split(':') 119 | if not re.search(r'^name=', line[1]): continue 120 | service = line[2] 121 | break 122 | return super(FlowInfo, cls).__new__(cls, pid or '?', uid, gid, cmdline, service) 123 | 124 | 125 | def get_flow_info(flow, _nx=FlowInfo(), _cache=dict()): 126 | _cache = _cache.setdefault(flow.proto, defaultdict(dict)) 127 | 128 | cache = _cache['sk'] 129 | ip_key = tuple(sorted([(flow.src, flow.sport), (flow.dst, flow.dport)])) 130 | if ip_key not in cache: 131 | cache.clear() 132 | cache.update(get_table_sk(flow.proto)) 133 | if ip_key not in cache: 134 | log.info('Failed to find connection for %s', ip_key) 135 | return _nx 136 | sk = cache[ip_key] 137 | 138 | cache = _cache['links'] 139 | if sk not in cache: 140 | cache.clear() 141 | cache.update(get_table_links()) 142 | if sk not in cache: 143 | log.info('Failed to find pid for %s', ip_key) 144 | return _nx 145 | pid = cache[sk] 146 | 147 | cache = _cache['info'] 148 | try: pid_ts = int(pid_info(pid, 'stat').split()[21]) 149 | except ProcReadFailure: 150 | log.info('Failed to query pid info for %s', ip_key) 151 | return _nx 152 | else: 153 | if pid in cache: 154 | info_ts, info = cache[pid] 155 | if pid_ts != info_ts: del cache[pid] # check starttime to detect pid rotation 156 | if pid not in cache: 157 | cache[pid] = pid_ts, FlowInfo(pid) 158 | info_ts, info = cache[pid] 159 | 160 | return info 161 | 162 | 163 | def main(argv=None): 164 | import argparse 165 | parser = argparse.ArgumentParser(description='conntrack event logging/audit tool.') 166 | parser.add_argument('-p', '--protocol', 167 | help='Regexp (python) filter to match "ev.proto". Examples: ipv4, tcp, ipv6/udp.') 168 | parser.add_argument('-t', '--format-ts', default='%s', 169 | help='Timestamp format, as for datetime.strftime() (default: %(default)s).') 170 | parser.add_argument('-f', '--format', 171 | default='{ts}: {ev.proto} {ev.src}/{ev.sport} > {ev.dst}/{ev.dport}' 172 | ' :: {info.pid} {info.uid}:{info.gid} {info.service} :: {info.cmdline}', 173 | help='Output format for each new flow, as for str.format() (default: %(default)s).') 174 | parser.add_argument('--debug', 175 | action='store_true', help='Verbose operation mode.') 176 | opts = parser.parse_args(argv or sys.argv[1:]) 177 | 178 | opts.format += '\n' 179 | 180 | import logging 181 | logging.basicConfig(level=logging.DEBUG if opts.debug else logging.WARNING) 182 | global log 183 | log = logging.getLogger() 184 | 185 | nfct = NFCT() 186 | 187 | # I have no idea why, but unless I init "all events" conntrack 188 | # socket once after boot, no events ever make it past NFNLGRP_CONNTRACK_NEW. 189 | # So just get any event here jic and then init proper handlers. 190 | src = nfct.generator() 191 | next(src) 192 | try: src.send(StopIteration) 193 | except StopIteration: pass 194 | 195 | src = nfct.generator(events=nfct.libnfct.NFNLGRP_CONNTRACK_NEW) 196 | netlink_fd = next(src) # can be polled, but not used here 197 | 198 | log.debug('Started logging') 199 | for ev_xml in src: 200 | try: ev = parse_event(ev_xml) 201 | except: 202 | log.exception('Failed to parse event data: %r', ev_xml) 203 | continue 204 | if not ev: continue 205 | if opts.protocol and not re.search(opts.protocol, ev.proto): continue 206 | log.debug('Event: %s', ev) 207 | sys.stdout.write(opts.format.format( ev=ev, 208 | ts=ev.ts.strftime(opts.format_ts), info=get_flow_info(ev) )) 209 | sys.stdout.flush() 210 | 211 | if __name__ == '__main__': sys.exit(main()) 212 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os, sys 4 | 5 | from setuptools import setup 6 | 7 | # Error-handling here is to allow package to be built w/o README included 8 | try: 9 | readme = open(os.path.join( 10 | os.path.dirname(__file__), 'README.txt' )).read() 11 | except IOError: readme = '' 12 | 13 | from nfct_cffi import NFCT 14 | 15 | setup( 16 | 17 | name = 'conntrack-logger', 18 | version = '13.07.0', 19 | author = 'Mike Kazantsev', 20 | author_email = 'mk.fraggod@gmail.com', 21 | license = 'WTFPL', 22 | keywords = [ 23 | 'nfct', 'conntrack', 'flow', 'connection', 'traffic', 'analysis', 24 | 'analyze', 'network', 'linux', 'security', 'track', 'netfilter', 25 | 'audit', 'cffi', 'libnetfilter_conntrack', 'netlink', 'socket' ], 26 | url = 'http://github.com/mk-fg/conntrack-logger', 27 | 28 | description = 'Tool to log conntrack flows and associated process/service info', 29 | long_description = readme, 30 | 31 | classifiers = [ 32 | 'Development Status :: 4 - Beta', 33 | 'Intended Audience :: Developers', 34 | 'Intended Audience :: System Administrators', 35 | 'Intended Audience :: Telecommunications Industry', 36 | 'License :: OSI Approved', 37 | 'Operating System :: POSIX :: Linux', 38 | 'Programming Language :: Python', 39 | 'Programming Language :: Python :: 2.7', 40 | 'Programming Language :: Python :: 2 :: Only', 41 | 'Topic :: Security', 42 | 'Topic :: System :: Networking :: Monitoring', 43 | 'Topic :: System :: Operating System Kernels :: Linux' ], 44 | 45 | install_requires = ['cffi'], 46 | ext_modules = [NFCT().ffi.verifier.get_extension()], 47 | 48 | py_modules = ['nfct_cffi', 'nfct_logger'], 49 | package_data = {'': ['README.txt']}, 50 | 51 | entry_points = { 52 | 'console_scripts': ['conntrack-logger = nfct_logger:main'] }) 53 | --------------------------------------------------------------------------------