ctfilterd or the Conntrack Filter Daemon is an open-source daemon software for Linux whose task/purpose is to provide filtered connection list from Netfilter's conntrack subsystem to clients via Unix/IP sockets in a well-defined and well-parseable CSV-like format. To be a little bit more specific, the daemon does the following each time a clients connects:
- reads and parses the
/proc/net/nf_conntrackfile containing the list of tracked connections - filters the connection list using a given logical expression with various possible matching criteria
- serializes and sends the filtered connection list to a client via a Unix or TCP socket in a CSV-like format
Being written in C++20 and making use of an epoll-based
event loop, the program shall be well-performant and able to handle many simultaneous client connections, while also
being relatively light on system resources. However, it should be noted that the program doesn't support any means of
client authentication, as it is designed mainly to serve local clients. If you plan to expose the daemon to anything
more than localhost, please make sure to configure your firewall properly or at least define the filter expression
in a way that all connections which are not supposed to be seen by public are left out.
ctfilterd only depends on the C++ STL and the standard C library (with POSIX and Linux extensions) – no
external libraries are required. The CMake build system is used, and as compiler it is recommended to use GCC
(g++) in version 12 or higher (clang should also work; either way, make sure your compiler version is
relatively new, as some relatively new C++ features are used within the codebase).
To build the program, run the following commands in the repository's root directory:
CXX=g++ cmake -S. -Bbuild
make -CbuildThe resulting binary will be located at ./build/ctfilterd. If desired, make -Cbuild install or
cmake --install build may then be used to install the compiled executable to your system.
ctfilterd takes all of its configuration from command-line parameters or environment variables (used if the relevant
command-line parameter is not given), i.e. the program does not use any configuration files. Strictly speaking, it is
not required to configure the program in any way (via parameters or the environment; in which case, the program will
listen on 127.0.0.1:7890 and provide information about all connections without any filtering to clients), but in the
vast majority of cases, you will want to change the defaults. There are also some build-time configuration options
available – see the ctfilterd.hpp file for their list.
The output of ./ctfilterd --help describes all the runtime configuration options and their purpose in detail:
Usage: ./ctfilterd [OPTION]... [FILTER_SPEC]...
Options:
-h, --help
Prints this help text and exits.
-v, --version
Prints version information and exits.
-l, --license
Prints license text and exits.
-L, --listen=LISTEN_ENDPOINT1[;LISTEN_ENDPOINT2]...
Specifies Unix, IPv4 or IPv6 socket addresses on which the program will
listen for client connections. See the 'Listen endpoints' section below
for more information on the format.
ENV: CTFILTERD_LISTEN (used if this option is not given)
DEFAULT: 'ip:127.0.0.1:7890'
-U, --user=USERNAME
Specifies the user to which the program will drop its privileges after
it initializes. If this option is not given, no user privilege drop will
happen.
ENV: CTFILTERD_USER (used if this option is not given)
-G, --group=GROUPNAME
Specifies the group to which the program will drop its privileges after
it initializes. If this option is not given, no group privilege drop will
happen.
ENV: CTFILTERD_GROUP (used if this option is not given)
-C, --ready-cmd=COMMAND
Specifies a command which the program will execute (using '/bin/sh') after
it creates all listening sockets, but before it drops its privileges. It may
be used, for example, to set permissions of a newly created Unix socket or
the '/proc/net/nf_conntrack' file. If this option is not given,
no command will be executed.
ENV: CTFILTERD_READY_CMD (used if this option is not given)
-D, --debug
If specified, the program will print various of debug messages to stderr.
ENV: CTFILTERD_DEBUG (used if this option is not given)
Listen endpoints:
The listen string, given via the '-L'/'--listen' command-line option, or
the 'CTFILTERD_LISTEN' environment variable, if the former is not given
contains one or more socket listen endpoints separated by the ';' (comma)
character, each in one of the following formats:
* unix:<path>
* ip:<ipv4_address>:<port>
* ip:[<ipv6_address>]:<port>
* ip:<hostname>:<port>
Filter specification:
The filter specification string, given via one or more (will be concatenated
using spaces) command-line arguments (should be specified after any
command-line options), or the 'CTFILTERD_FILTER' environment variable, if the
former is not given, shall conform to the following grammar (in EBNF
notation) - 'disjunct' is the starting non-terminal. In addition, the parser
ignores all whitespace characters (but is case-sensitive), and an empty
string is allowed, meaning that no filter will be applied.
disjunct = conjunct, { '||', conjunct }
conjunct = negate, { '&&', negate }
negate = [ '!' ], expr_or_check
expr_or_check = ( '(', disjunct, ')' ) | check
check = ip_ver_check | ip_addr_check | proto_check | port_check | icmp_check | state_check
ip_ver_check = ( 'ip_version' | 'ip_ver' ), eq_op, ( 'ipv4' | 'v4' | '4' | 'ipv6' | 'v6' | '6' )
ip_addr_check = ( 'src_ip' | 'dst_ip' ), eq_op, subnet
proto_check = ( 'proto' | 'ip_proto' ), eq_op, ( 'tcp' | 'udp' | 'icmpv6' | 'icmp' | num_range )
port_check = ( 'src_port' | 'dst_port' ), eq_op, num_range
icmp_check = ( 'icmp_type' | 'icmp_code' ), eq_op, num_range
state_check = ( 'state' | 'conn_state' ), eq_op, state_name
eq_op = '==' | '!='
subnet = ip_addr, [ '/', prefix_length ]
num_range = number, [ '-', number ]
ip_addr = ? IPv4 or IPv6 network address in standard text notation ?
prefix_length = ? subnet prefix length in standard decimal notation, as in for example '.../24', '.../64', ... ?
state_name = ? an upper-case state name of a transport protocol, e.g. 'SYN_SENT', 'ESTABLISHED', 'TIME_WAIT', ... ?
number = ? a non-empty sequence of decimal digits ?
The program may be terminated using SIGTERM or SIGINT.
The text-based protocol, which ctfilterd provides on configured Unix or TCP listen endpoints, is very simple – when a client connects, the server immediately sends them the current filtered connection list in the format described below, and then immediately closes the connection. The client is not supposed to send any data to the server – the server simply won't receive them.
The response consists of the following three parts:
- the first line – preamble:
ctfilterd v1 - the following (zero or more) lines – conntrack entry (as described below)
- the last line – an empty line
Each conntrack entry is represented/described by a single line in the following CSV-like format (the \ is not
literal – it is used only here, in this documentation, as a newline-escape character):
<"ipv4"/"ipv6">,<l4_proto_num>,<src_ip>,<dst_ip>,[src_port],[dst_port],[icmp_type],[icmp_code],\
<timeout>,<packets_from_src>,<packets_from_dst>,<bytes_from_src>,<bytes_from_dst>,[state]
Fields annotated with <...> are mandatory, i.e. always contain their respective value, whereas fields annotated with
[...] are optional – if they don't contain any value, they contain - (a single dash/minus character) instead:
"ipv4"/"ipv6": The L3 protocol version – eitheripv4oripv6l4_proto_num: The L4 protocol number, i.e.6for TCP,17for UDP, etc.src_ip: The IPv4/IPv6 address of the host who initiated the connectiondst_ip: The IPv4/IPv6 address of the destination hostsrc_port(optional): The port on the initiator's side (-if the L4 protocol does not use ports)dst_port(optional): The port on the destination's side (-if the L4 protocol does not use ports)icmp_type(optional): The ICMPv4/v6 type number (-if the connection is not a ICMP one)icmp_code(optional): The ICMPv4/v6 code number (-if the connection is not a ICMP one)timeout: Number of seconds after which the connection will stop to be tracked if idlepackets_from_src: Total number of packets sent by the initiator hostpackets_from_dst: Total number of packets sent by the destination hostbytes_from_src: Total number of bytes sent by the initiator hostbytes_from_dst: Total number of bytes sent by the destination hoststate(optional): A string describing the current state of the connection (consisting of uppercase letters and underscores – e.g.ESTABLISHED,SYN_SENT,TIME_WAIT, ...;-if the L4 protocol has no states)
If the connection is closed by the server without sending any data, it means that an error has occurred while
the server was preparing the response (e.g. it could not open /proc/net/nf_conntrack due to its non-existence or
insufficient permissions) – if you run the program with --debug, you should see an error message with the
reason printed to your terminal.
-
Only
localhostconnections:(src_ip == 127.0.0.0/8 && dst_ip == 127.0.0.0/8) || (src_ip == ::1 && dst_ip == ::1) -
Only established HTTP(S) connections from
192.168.1.0/24:src_ip == 192.168.1.0/24 && proto == tcp && (dst_port == 80 || dst_port == 443) && state == ESTABLISHED -
Only ICMPv4/ICMPv6
pingsessions:((ip_ver == v4 && proto == icmp && (icmp_type == 8 || icmp_type == 0)) || (ip_ver == v6 && proto == icmpv6 && icmp_type == 128-129)) && icmp_code == 0 -
TCP, UDP or SCTP connections originating from privileged ports:
(proto == tcp || proto == udp || proto == 132) && src_port == 1-1023 -
Connections not destined to
192.88.99.1:dst_ip != 192.88.99.1
-
Unix socket at
/run/ctfilterd/ctfilterd.sock:unix:/run/ctfilterd/ctfilterd.sock -
127.0.0.1:7890and[::1]:7890:ip:127.0.0.1:7890;ip:[::1]:7890 -
Unix socket at
/run/ctfilterd/ctfilterd.sockand all IPv4/IPv6 addresses ofexample.comat port1234:unix:/run/ctfilterd/ctfilterd.sock;ip:example.com:1234
ctfilterd v1
ipv6,58,::1,::1,-,-,128,0,29,21,21,2184,2184,-
ipv4,6,127.0.0.1,127.0.0.1,45448,7890,-,-,9,5,4,269,453,CLOSE
ipv4,1,127.0.0.1,127.0.0.1,-,-,8,0,29,15,15,1260,1260,-
ipv4,6,127.0.0.1,127.0.0.1,45454,7890,-,-,432000,2,1,112,60,ESTABLISHED
ipv6,6,::1,::1,49932,1234,-,-,431967,2,1,152,80,ESTABLISHED
This project is licensed under the 3-clause BSD license – see the LICENSE file.
Programmed by Vít Labuda.
This project was created as part of my desire to practice programming in the C++ language after attending a C++ course at my university, where I learned quite a lot of stuff and was worried that I would forget it (this project itself was not part of the course, it was created as a result of my own initiative). For this reason, this program is a bit too complicated for what it is supposed to do, but this has to do with the fact that the main objective here was to practice both C++ programming (object ownership and lifecycle, OOP and virtual methods, RAII, exceptions, string and I/O handling, ...) and theoretical computer science (formal grammars and parsers) concepts, and not to create the simplest solution for the thing I personally use this program for (a web app in my internal network) – for that, a Python script would probably suffice.