Skip to content

vitlabuda/ctfilterd

Repository files navigation

ctfilterd

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_conntrack file 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.

Build

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 -Cbuild

The 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.

Deployment guides

Usage

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.

Client protocol

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 – either ipv4 or ipv6
  • l4_proto_num: The L4 protocol number, i.e. 6 for TCP, 17 for UDP, etc.
  • src_ip: The IPv4/IPv6 address of the host who initiated the connection
  • dst_ip: The IPv4/IPv6 address of the destination host
  • src_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 idle
  • packets_from_src: Total number of packets sent by the initiator host
  • packets_from_dst: Total number of packets sent by the destination host
  • bytes_from_src: Total number of bytes sent by the initiator host
  • bytes_from_dst: Total number of bytes sent by the destination host
  • state (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.

Examples

Filter string/expression

  • Only localhost connections:

    (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 ping sessions:

    ((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
    

Listen endpoints

  • Unix socket at /run/ctfilterd/ctfilterd.sock:

    unix:/run/ctfilterd/ctfilterd.sock
    
  • 127.0.0.1:7890 and [::1]:7890:

    ip:127.0.0.1:7890;ip:[::1]:7890
    
  • Unix socket at /run/ctfilterd/ctfilterd.sock and all IPv4/IPv6 addresses of example.com at port 1234:

    unix:/run/ctfilterd/ctfilterd.sock;ip:example.com:1234
    

Server response

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

Licensing

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.

About

Conntrack Filter Daemon

Resources

License

Stars

Watchers

Forks