© 2022 Ian Pilcher <[email protected]>
- Introduction
- Filter API Header
- Building, Installing, and Using a Filter Module
- Filter Development Best Practices
FDF filters are dynamically loaded modules that extend the functionality of the service. FDF includes three filters — the multicast DNS filter, the IP set filter, and the nftables set filter, but additional filters can be created using the APIs described in this document.
The FDF filter API is defined by its header file — fdf-filter.h
. This
file is located in the src
directory of this repository. When building
"out of tree" filters, it should be copied to the system header file directory,
usually /usr/include
.
The header file defines the FDF_FILTER_API_VER
constant (macro), which is
used to verify compatibility between the FDF daemon and any filter module that
it loads. The API does not currently support backward compatibility, so the
versions must match exactly.
The value of this macro is computed from the MD5 hash of the header file itself,
excepting the value of the FDF_FILTER_API_VER
macro (and the value of the
FDF_FILTER_CTOR
macro, which is also derived from the hash). As a result,
any changes to the header, including changes to formatting or comments,
will change the value of the API version. Because the API version must match
exactly, this will require all filter modules to be rebuilt with the new API
version.
When a filter module is loaded by the daemon, it must register itself by calling
the fdf_filter_register()
API.
struct fdf_filter_info {
uint64_t api_ver;
fdf_filter_init_fn init_fn;
fdf_filter_match_fn match_fn;
fdf_filter_cleanup_fn cleanup_fn;
};
__attribute__((nonnull))
void fdf_filter_register(const struct fdf_filter_info *info);
api_ver
must be set to FDF_FILTER_API_VER
. init_fn
, match_fn
,
and cleanup_fn
are pointers to filter functions that fdfd
will call at
different times in the module's lifetime.
-
The initialization function (
init_fn
) will be called once for each instance of the filter module when that instance is created. It is optional (may beNULL
) for filter modules that do not accept any parameters. -
The match function (
match_fn
) is required. It will be called for each packet received by a listener whose match includes an instance of the filter module in its filter chain (unless a filter instance earlier in the chain stops filter execution by returningFDF_FILTER_PASS_NOW
orFDF_FILTER_DROP_NOW
). -
The cleanup function (
cleanup_fn
) is optional. It will be called once for each instance of the filter module when the FDF daemon is shutting down cleanly.
fdf_filter_register()
should be called from a constructor function, which will
automatically be called when the filter module is loaded.
static struct fdf_filter_info foo_info {
.api_ver = FDF_FILTER_API_VER,
.init_fn = foo_init,
.match_fn = foo_match,
.cleanup_fn = foo_cleanup
};
__attribute__((constructor))
static void foo_ctor(void)
{
fdf_filter_register(&foo_info);
}
The header file defines a macro that eliminates the need for this boilerplate code. The macro should be used in most cases.
FDF_FILTER(foo_init, foo_match, foo_cleanup);
The initialization function is used to perform any setup required by an instance of a filter module, which may include "global" setup required by the module itself. It must conform to the following type definition.
typedef _Bool (*fdf_filter_init_fn)(uintptr_t handle,
int argc, const char *const argv[]);
-
handle
— An opaque value that identifies the filter instance. It must be passed back to any FDF filter API functions called from the filter module. -
argc
— The number of non-NULL
members ofargv
. (argv[argc]
is alwaysNULL
.) The minimum value ofargc
is2
, because the name of the filter instance and the path of the filter module file are always present. -
argv
— A pointer to aNULL
-terminated array of character pointers. Each member of the array (other than the terminatingNULL
member) points to a C string that holds the name of the filter instance, the path used to load the filter module, or a filter instance parameter.argv[0]
points to the name of the filter instance.argv[1]
points to the path of the filter module file.argv[2]
throughargv[argc - 1]
point to the filter instance parameters, if any.
NOTE: Unlike the
argv
argument to C'smain()
function, the array itself and the strings to which its members are allconst
typed.
The initialization function should return 1
to indicate successful
initialization or 0
if an error occured.
static _Bool foo_init(const uintptr_t handle,
const int argc __attribute__((unused)),
const char *const *const argv)
{
fdf_filter_log(handle, LOG_DEBUG, "Instance name = %s", argv[0]);
return 1;
}
The cleanup function is used to free any resources (memory allocations, open file descriptors, etc.) that were acquired by a filter instance, including any "global" resources that are shared between instances. It must conform to the following definition.
typedef void (*fdf_filter_cleanup_fn)(uintptr_t handle);
handle
— An opaque value that identifies the filter instance. It must be passed back to any FDF filter API functions called from the filter module.
static void foo_cleanup(const uintptr_t handle)
{
fdf_filter_log(handle, LOG_DEBUG, "All done");
}
The match function is the workhorse of any filter module. It is called each
time that a packet is received by a listener whose
match includes an instance of the filter module (unless a
filter instance earlier in the chain stops filter execution by returning
FDF_FILTER_PASS_NOW
or
FDF_FILTER_DROP_NOW
).
The match function should parse the packet payload (if necessary), determine whether the packet should be passed or dropped, and perform any other actions that are required.
Examples of other actions include:
-
In stateful mode, the multicast DNS filter adds information about any queries that it forwards to a global data structure. When an mDNS response it received, this data structure is used to determine the network to which the response should be forwarded (if any).
-
The IP set and nftables set filters add the source address and source port of any packets that they processe to a kernel IP set or nftables set.
The match function must conform to the following (rather ugly) definition.
typedef
uint8_t (*fdf_filter_match_fn)(uintptr_t handle,
const struct sockaddr_storage *restrict src,
const struct sockaddr_storage *restrict dest,
const void *restrict pkt, size_t pkt_size,
uintptr_t in_netif, uintptr_t *fwd_netif_out);
-
handle
— An opaque value that identifies the filter instance. It must be passed back to any FDF filter API functions called from the filter module. -
src
— The source address (IP address and UDP port number) of the packet. -
dest
— The destination address (broadcast or multicast IP address and UDP port number) of the packet. -
pkt
— The packet payload (not including the IP and UDP headers). SeeFDF_FILTER_PKT_AS()
. -
pkt_size
— The size (in bytes) of the packet payload. -
in_netif
— An opaque value that identifies the network interface on which the packet was received. -
fwd_netif_out
— An output pointer that can be used to set the network interface to which a packet will be forwarded. The value written via the pointer must have previously been passed in thein_netif
argument.It is an error for a filter instance to set a forward interface that is not valid for the listener that received the packet. It is also an error for more than one filter instance in a chain to set the forward interface.
The match function must return one of the following values.
-
FDF_FILTER_PASS
— Forward the packet if this is the last filter instance in the listener's filter chain. If it is not last in the chain, the disposition of the packet will be determined by the result(s) of the subsequent filter(s). -
FDF_FILTER_DROP
— Drop the packet if this is the last filter instance in the listener's filter chain. If it is not last in the chain, the disposition of the packet will be determined by the result(s) of the subsequent filter(s). -
FDF_FILTER_PASS_FORCE
— Forward the packet, unless a subsequent filter instance returnsFDF_FILTER_DROP_FORCE
orFDF_FILTER_DROP_NOW
. -
FDF_FILTER_DROP_FORCE
— Drop the packet, unless a subsequent filter instance returnsFDF_FILTER_PASS_FORCE
orFDF_FILTER_PASS_NOW
. -
FDF_FILTER_PASS_NOW
— Forward the packet immediately. Ignore any subsequent filter instances in the listener's filter chain. -
FDF_FILTER_DROP_NOW
— Drop the packet immediately. Ignore any subsequent filter instances in the listener's filter chain.
static uint8_t foo_match(const uintptr_t handle,
const struct sockaddr_storage *restrict const src
__attribute__((unused)),
const struct sockaddr_storage *restrict const dest
__attribute__((unused)),
const void *restrict const pkt __attribute__((unused)),
const size_t pkt_size __attribute__((unused)),
const uintptr_t in_netif __attribute__((unused)),
uintptr_t *const fwd_netif_out __attribute__((unused)))
{
/* Drop 10% of the packets */
if (rand() % 100 < 10) {
fdf_filter_log(handle, LOG_INFO, "Dropping unlucky packet");
return FDF_FILTER_DROP;
}
else {
fdf_filter_log(handle, LOG_DEBUG, "Passing packet");
return FDF_FILTER_PASS;
}
}
The FDF daemon provides a number of helper APIs that filter modules may use.
__attribute__((format(printf, 3, 4), nonnull))
void fdf_filter_log(uintptr_t handle, int priority,
const char *restrict format, ...);
Log a message via the FDF daemon. The message may be suppressed in some circumstances.
-
If
priority
isLOG_DEBUG
and the daemon was not executed with the-d
(or--debug
) option. -
If
fdf_filter_log()
is called from the filter module's match function (or a function called from the match function, etc.),priority
isLOG_INFO
orLOG_DEBUG
, and the daemon was not executed with the-p
(or--pktlog
) option. (Both-d
and-p
must be specified in order to seeLOG_DEBUG
messages issued from within a filter module's match function.)
-
handle
— Thehandle
value that was passed to the module's initialization, match, or cleanup function. -
priority
— Asyslog(3)
-style constant that identifies the severity of the message —LOG_DEBUG
,LOG_INFO
, ...,LOG_EMERG
. -
format
— Aprintf(3)
-style format string for the message. (No trailing newline is required; the daemon will add it to the final message if needed.) -
...
— Additionalprintf(3)
-style arguments (if any) that match the format string.
__attribute__((nonnull))
const char *fdf_filter_sock_addr(uintptr_t handle,
const struct sockaddr_storage *restrict addr,
char *restrict dst, size_t size);
Converts the IPv4 or IPv6 socket address in addr
to a textual representation
(C string) in dst
. IPv4 socket addresses are formatted in standard dotted
decimal format, followed by a colon and the decimal port number — e.g.
224.0.0.251:5353
; IPv6 socket addresses place the canonical form of the IPv6
address within square brackets, followed by a colon and the decimal port number
— e.g. [ff02::fb]:5353
.
NOTE: FDF does not currently support IPv6.
-
handle
— Thehandle
value that was passed to the module's initialization, match, or cleanup function. -
addr
— The address to be formatted. -
dst
— The buffer into which the formatted address will be placed. The buffer size must be at leastFDF_FILTER_SA4_LEN
(if formatting an IPv4 socket address) orFDF_FILTER_SA6_LEN
(if formatting an IPv6 socket address). -
size
— The size of the destination buffer.
Returns dst
.
const char *fdf_filter_netif_name(uintptr_t handle, uintptr_t netif);
Retrieves the name of the network interface identified by netif
.
-
handle
— Thehandle
value that was passed to the module's initialization, match, or cleanup function. -
netif
— An opaque network interface identifier that was passed in the match function'sin_netif
argument.
A pointer to a C string that contains the name of the network interface.
union fdf_filter_data {
void *p;
uintptr_t u;
intptr_t i;
_Bool b;
};
void fdf_filter_set_data(uintptr_t handle, union fdf_filter_data data);
Associates arbitrary data with a filter instance. The data can be retrieved
with fdf_filter_get_data()
.
-
handle
— Thehandle
value that was passed to the module's initialization, match, or cleanup function. -
data
— The data to be associated with the filter instance.
union fdf_filter_data fdf_filter_get_data(uintptr_t handle);
Retrieves data that that was previously associated with the filter instance by
fdf_filter_set_data()
.
handle
— Thehandle
value that was passed to the module's initialization, match, or cleanup function.
The data that was most recently associated with the filter instance.
#define FDF_FILTER_PKT_AS(type, pkt) \
({ \
_Static_assert(__alignof__(type) <= 4, \
"alignment of " #type " too large"); \
(const type *)pkt; \
})
Casts pkt
(the packet payload that was passed to the match function) as a
pointer to const type
. Issues a compile-time error if type
's alignment is
too large.
For example, the code below will cause a compile-time error on 64-bit platforms,
because the alignment of struct my_pkt
is too large.
struct my_pkt {
uint64_t magic_number; /* 8-byte alignment on 64-bit */
uint8_t data[];
};
/* Called from initialization function */
static void check_pkt(const void *restrict const pkt)
{
struct my_pkt *p;
p = FDF_FILTER_PKT_AS(struct my_pkt, pkt);
}
-
type
— The C type that will be used to process the packet payload. -
pkt
— The packet payload (the match function'spkt
argument).
A pointer to the packet payload, cast to a pointer to const type
.
Consider the following simple filter (foo.c
), which combines the examples
above.
#include <fdf-filter.h>
#include <syslog.h>
static _Bool foo_init(const uintptr_t handle,
const int argc __attribute__((unused)),
const char *const *const argv)
{
fdf_filter_log(handle, LOG_DEBUG, "Instance name = %s", argv[0]);
return 1;
}
static void foo_cleanup(const uintptr_t handle)
{
fdf_filter_log(handle, LOG_DEBUG, "All done");
}
static uint8_t foo_match(const uintptr_t handle,
const struct sockaddr_storage *restrict const src
__attribute__((unused)),
const struct sockaddr_storage *restrict const dest
__attribute__((unused)),
const void *restrict const pkt __attribute__((unused)),
const size_t pkt_size __attribute__((unused)),
const uintptr_t in_netif __attribute__((unused)),
uintptr_t *const fwd_netif_out __attribute__((unused)))
{
/* Drop 10% of the packets */
if (rand() % 100 < 10) {
fdf_filter_log(handle, LOG_INFO, "Dropping unlucky packet");
return FDF_FILTER_DROP;
}
else {
fdf_filter_log(handle, LOG_DEBUG, "Passing packet");
return FDF_FILTER_PASS;
}
}
FDF_FILTER(foo_init, foo_match, foo_cleanup);
To build the module, simply compile it with the -shared
and -fPIC
options.
For example:
$ gcc -std=gnu99 -O3 -Wall -Wextra -Wcast-align -shared -fPIC -o foo.so foo.c
NOTE: See the note here about the
-std=gnu99
and-Wcast-align
options.
The FDF daemon does not search any particular directory (other than the system's standard library directories) for filter modules; the paths to all filter module files must be specified in the configuration. Thus, there is no particular location to which filter modules must be installed. The recommended practice, however, is to place all filter modules in a single directory:
-
/usr/local/lib/fdf-filters
or/usr/local/lib64/fdf-filters
if the module is manually installed, or -
/usr/lib/fdf-filters
or/usr/lib64/fdf-filters
if the module is installed with a system package manager.
The filter module can be used by including it in the FDF configuration. For example:
"filters": {
"foo": {
"file": "/usr/local/lib64/fdf-filters/foo.so"
}
}
-
Don't directly assign (or cast) the match function's
pkt
argument to a typed pointer. Use theFDF_FILTER_PKT_AS()
macro. -
Use the
fdf_filter_log()
function for any error or informational messages. -
Ensure that the cleanup function frees all resources that the filter module acquires during its lifetime, including any module-wide resources that are shared between instances. The FDF daemon itself has no known memory or file descriptor leaks, so tools such as
valgrind
can be used to check for resource leaks.