© 2022 Ian Pilcher <arequipeno@gmail.com>
- Introduction
- Step 1: Prepare the Build System
- Step 2: Prepare the Build Environment
- Step 3: Build
libsavl
and FDF - Step 4: Install
libsavl
and FDF - Step 5: Configure the Device Network and Firewall
- Step 6: Final Steps
OpenWrt is a popular Linux-based alternative operating system for residential routers. As such, it is an obvious target for FDF. This document provides build and installation instructions for OpenWrt.
NOTES:
Support for OpenWrt is experimental. I personally use my OpenWrt device only as a wireless access point, not as a router, so I don't run FDF on OpenWrt. (I run FDF on my firewall/router, which is an x86-based Protectli Vault running CentOS 7.)
Building software for OpenWrt is a somewhat complex process. The software must be cross compiled, because the target devices are not usually powerful enough for software development (even if you did want to install the required development tools, library headers, etc., on your router).
Furthermore, the easiest way to set up a cross compilation environment is to go through the process of building OpenWrt itself. As the OpenWrt build documentation states, "The process of creating a cross compiler can be tricky. It's not something that's regularly attempted and so the there's a certain amount of mystery and black magic associated with it." The process is also fairly resource-intensive. I recommend placing the build directory on a file system with at least 15 GB free. If possible, use a system with at least 4 CPU cores and 8 GB of memory available.
Follow the instructions here to prepare your system.
On my Fedora system, for example:
$ sudo dnf --setopt install_weak_deps=False --skip-broken install \
bash-completion bzip2 gcc gcc-c++ git make ncurses-devel patch \
rsync tar unzip wget which diffutils python2 python3 perl-base \
perl-Data-Dumper perl-File-Compare perl-File-Copy perl-FindBin \
perl-Thread-Queue
Last metadata expiration check: 1:27:33 ago on Mon 28 Mar 2022 01:08:36 PM CDT.
Package bash-completion-1:2.11-3.fc35.noarch is already installed.
Package bzip2-1.0.8-9.fc35.x86_64 is already installed.
⋮
Package perl-Thread-Queue-3.14-478.fc35.noarch is already installed.
Dependencies resolved.
Nothing to do.
Complete!
Download the OpenWrt source. Change to the source repository's top-level directory (the "build directory"), and checkout the tag that corresponds to the OpenWrt version that your device is running.
NOTE: The examples below are all written for my personal device, a TP-Link Archer C7 AC1750 v2 running OpenWrt 21.02.2.
$ git clone https://git.openwrt.org/openwrt/openwrt.git
Cloning into 'openwrt'...
remote: Enumerating objects: 7248, done.
remote: Counting objects: 100% (7248/7248), done.
remote: Compressing objects: 100% (5439/5439), done.
remote: Total 595885 (delta 4025), reused 2676 (delta 1505), pack-reused 588637
Receiving objects: 100% (595885/595885), 175.36 MiB | 11.61 MiB/s, done.
Resolving deltas: 100% (415585/415585), done.
$ cd openwrt
$ git tag
⋮
v21.02.0-rc4
v21.02.1
v21.02.2
$ git checkout v21.02.2
HEAD is now at 30e2782e06 OpenWrt v21.02.2: adjust config defaults
Update the "feeds." Both of these commands will generate a lot of output, so check the exit codes.
$ ./scripts/feeds update -a
Updating feed 'packages' from 'https://git.openwrt.org/feed/packages.git^b0ccc356900f6e1e1dc613d0ea980d5572f553dd' ...
Cloning into './feeds/packages'...
remote: Enumerating objects: 160727, done.
⋮
Create index file './feeds/telephony.index'
Collecting package info: done
Collecting target info: done
$ echo $?
0
$ ./scripts/feeds install -a
Collecting package info: done
Collecting target info: done
WARNING: Makefile 'package/utils/busybox/Makefile' has a dependency on 'libpam', which does not exist
⋮
Installing package 'siproxd' from telephony
Installing package 'sngrep' from telephony
Installing package 'yate' from telephony
$ echo $?
0
Follow the instructions here to download the official build config for your device.
$ wget https://downloads.openwrt.org/releases/21.02.1/targets/ath79/generic/config.buildinfo -O .config
--2022-03-28 14:30:52-- https://downloads.openwrt.org/releases/21.02.1/targets/ath79/generic/config.buildinfo
Resolving downloads.openwrt.org (downloads.openwrt.org)... 168.119.138.211, 2a01:4f8:251:321::2
Connecting to downloads.openwrt.org (downloads.openwrt.org)|168.119.138.211|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 39799 (39K) [text/plain]
Saving to: ‘.config’
.config 100%[==================================>] 38.87K --.-KB/s in 0.004s
2022-03-28 14:30:53 (10.3 MB/s) - ‘.config’ saved [39799/39799]
Run make menuconfig
. This will bring up an
ncurses-based interface that can be
used to modify the OpenWrt configuration.
As noted in Using the official build config, the build config does not specify a target profile. Instead, it enables all of the target profiles for a given target and subtarget. Presumably, this allows the OpenWrt project to create images for multiple similar devices in a single build, but it unnneccessarily increases the build time and storage consumption in this situation.
-
At the top-level OpenWrt Configuration menu, use the arrow keys to highlight Target Profile (Multiple devices) and press the enter key (
↵
) to select the submenu. -
In the Target Profile submenu, scroll down to locate your device. (Entries are not listed in strictly alphabetical order.)
-
Highlight the correct device and press
↵
to select it and return to the OpenWrt Configuration menu.
libmnl
is required by
FDF's IP set filter, but it is not included in OpenWrt images
by default, so it won't normally be built when building OpenWrt.
-
At the OpenWrt Configuration menu, highlight Libraries and press
↵
to select it. -
In the Libraries submenu, highlight libmnl and press
Y
to enable it. -
Press the right arrow key 3 times to highlight
<Save>
and press↵
. -
Press
↵
again to save the configuration as.config
. -
Press
↵
one more time to return to the Libraries submenu. -
Press
Esc
four times to exit from the configuration tool.
Determine the number of concurrent jobs to be used during the build process. For the fastest build, choose a number that is equal to, or slightly greater than, the number of logical processors (threads) in your system. Choose a lower number to reserve some processor capacity for other workloads. The number of jobs also impacts memory consumption, so it may be necessary to limit the number on a memory constrained system with a large number of logical processors.
nice
and ionice
can also be used to limit the impact of the OpenWrt build on
the rest of the system.
$ make -j18
make[2]: Entering directory '/mnt/scratch/openwrt/scripts/config'
cc -O2 -c -o conf.o conf.c
cc conf.o confdata.o expr.o lexer.lex.o parser.tab.o preprocess.o symbol.o util.o -o conf
make[2]: Leaving directory '/mnt/scratch/openwrt/scripts/config'
time: target/linux/prereq#0.60#0.05#0.65
make[1] world
⋮
make[2] package/index
make[2] json_overview_image_info
make[2] checksum
$ echo $?
0
The OpenWrt build process created the cross compiler, tools, and libraries
needed to build FDF (and libsavl
, upon which it depends). The files are
located in several different directories, and the paths to those directories are
quite long. The actual build process is much simpler if some important file and
directory paths are stored in shell variables.
All of the paths are located under the top-level directory of the OpenWrt repository that was created when the repository was cloned in step 2a.
Change to the build directory and store its path in a shell variable named
BUILD_DIR
, for example:
$ cd /mnt/scratch/openwrt
$ BUILD_DIR=`pwd`
NOTES:
/mnt/scratch/openwrt
is the build directory on my system.The
BUILD_DIR
shell variable is not the same as thebuild_dir
subdirectory discussed in the OpenWrt documentation. (build_dir
is a subdirectory of${BUILD_DIR}
.)
The staging directory contains the cross compiler and other tools that run on
the build host to create binary files compatible with the OpenWrt device. It
is located at
${BUILD_DIR}/staging_dir/toolchain-${CPU_ARCH}_gcc-${GCC_VER}_${C_LIB}
, where
${CPU_ARCH}
, ${GCC_VER}
, and ${C_LIB}
depend on the version of OpenWrt
and the device for which it was built.
For example, my TP-Link Archer C7 AC1750 v2 has a
MIPS 24Kc
processor, and OpenWrt 21.02.2 uses GCC 8.4 and the
musl C library. Therefore, the staging directory is
${BUILD_DIR}/staging_dir/toolchain-mips_24kc_gcc-8.4.0_musl
.
$ ls staging_dir
host packages toolchain-_gcc-_
hostpkg target-mips_24kc_musl toolchain-mips_24kc_gcc-8.4.0_musl
$ STAGING_DIR=${BUILD_DIR}/staging_dir/toolchain-mips_24kc_gcc-8.4.0_musl
Export the STAGING_DIR
variable, so that it will be visible to the cross
compiler.
$ export STAGING_DIR
NOTE: The following commands illustrate the difference between a shell variable (a variable that has not been exported) and an environment variable.
$ FOO=bar $ echo $FOO bar $ bash -c 'echo $FOO'
This produces no output, because the variable
FOO
is not visible in the child shell. (The single quotes prevent the variable from being expanded by the parent shell.)$ export FOO $ bash -c 'echo $FOO' bar
Now the child shell is able to expand
$FOO
, because the variable is visible.
The cross compiler executable is a regular (non-symlink) file in the
${STAGING_DIR}/bin
directory. It is the only regular file whose name ends
with -gcc
. Store the cross compiler's complete path in a shell variable named
XGCC
.
$ XGCC=`find ${STAGING_DIR}/bin -name '*-gcc' -type f`
$ echo ${XGCC}
/mnt/scratch/openwrt/staging_dir/toolchain-mips_24kc_gcc-8.4.0_musl/bin/mips-openwrt-linux-musl-gcc
NOTE: The output of the
echo
command will begin with your build directory path, which may not be/mnt/scratch/openwrt
.
The library directory contains shared libraries that were built for the OpenWrt
device by the OpenWrt build process. It is located at
${BUILD_DIR}/staging_dir/target-${CPU_ARCH}_${C_LIB}/usr/lib
. When OpenWrt
21.02.2 (musl C library) is built for my TP-Link Archer C7 AC1750 v2 (MIPS 24Kc
processor), the path is
${BUILD_DIR}/staging_dir/target-mips_24kc_musl/usr/lib
.
$ ls staging_dir
host packages toolchain-_gcc-_
hostpkg target-mips_24kc_musl toolchain-mips_24kc_gcc-8.4.0_musl
$ ls staging_dir/target-mips_24kc_musl/usr/lib
cmake liblucihttp.so libpcre32.so
libacl.a liblucihttp.so.0 libpcre32.so.0
libacl.so liblucihttp.so.0.1 libpcre32.so.0.0.12
⋮
liblualib.so libpcre16.so.0 pkgconfig
liblua.so libpcre16.so.0.2.12 terminfo
liblua.so.5.1.5 libpcre32.a
$ LIB_DIR=${BUILD_DIR}/staging_dir/target-mips_24kc_musl/usr/lib
NOTE: The CPU architecture of a binary file can be shown with the
file
utility.$ file $XGCC /mnt/scratch/openwrt/staging_dir/toolchain-mips_24kc_gcc-8.4.0_musl/bin/mips-openwrt-linux-musl-gcc: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux -x86-64.so.2, BuildID[sha1]=a770dca0917bdc6a7582414cb91393d6020812ad, for GNU/Linux 3.2.0, with debu g_info, not stripped $ file ${LIB_DIR}/libpcre16.so.0.2.12 /mnt/scratch/openwrt/staging_dir/target-mips_24kc_musl/usr/lib/libpcre16.so.0.2.12: ELF 32-bit MSB sh ared object, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, with debug_info, not stripped
Library header files are located in
${BUILD_DIR}/staging_dir/target-${CPU_ARCH}_${C_LIB}/usr/include
(${BUILD_DIR}/staging_dir/target-mips_24kc_musl/usr/include
for my device.
$ HEADER_DIR=`echo ${LIB_DIR} | sed 's/lib$/include/'`
$ echo ${HEADER_DIR}
/mnt/scratch/openwrt/staging_dir/target-mips_24kc_musl/usr/include
$ ls ${HEADER_DIR}
acl gdbm.h libubus.h ncurses.h term.h
argp.h gmp.h libunwind-common.h ncursesw ubi-media.h
atmarpd.h ip6tables.h libunwind-coredump.h net ubi-user.h
⋮
expat.h libubi.h menu.h swlib.h zlib.h
form.h libubi-tiny.h nbpf.h sys
fts.h libubox ncurses_dll.h termcap.h
libsavl
provides a lightweight
AVL tree implementation, which FDF
uses for various data structures.
From the build directory, clone the repository and change to its top-level directory.
$ cd ${BUILD_DIR}
$ git clone https://github.com/ipilcher/libsavl.git
Cloning into 'libsavl'...
remote: Enumerating objects: 79, done.
remote: Counting objects: 100% (75/75), done.
remote: Compressing objects: 100% (52/52), done.
remote: Total 79 (delta 34), reused 60 (delta 20), pack-reused 4
Receiving objects: 100% (79/79), 64.97 KiB | 1.33 MiB/s, done.
Resolving deltas: 100% (34/34), done.
$ cd libsavl
Determine the Library Version and soname
The full library version and can be determined from the latest tag in the repository. For example:
$ git describe --tags --abbrev=0
v0.7.1
In this case, the full library version is 0.7.1
(the numeric portion of the
name of the Git tag). The shared object version is 0.7
(the first two
elements of the library version), and the soname is libsavl.so.0.7
.
Use the cross compiler to build the library. Use the full library version in
the name of the output file (e.g., -o libsavl.so.0.7.1
).
$ $XGCC -O3 -Wall -Wextra -Wcast-align -shared -fPIC -I${HEADER_DIR} \
-o libsavl.so.0.7.1 savl.c -Wl,-soname=libsavl.so.0.7
$ file libsavl.so.0.7.1
libsavl.so.0.7.1: ELF 32-bit MSB shared object, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically link
ed, with debug_info, not stripped
$ objdump -p libsavl.so.0.7.1 | grep SONAME
SONAME libsavl.so.0.7
The library (and its header file) must be installed before it can be used to build FDF. First, copy the library to the library directory and create two symbolic links, one that matches the library's soname and one that is unversioned.
$ cp libsavl.so.0.7.1 ${LIB_DIR}
$ ln -s libsavl.so.0.7.1 ${LIB_DIR}/libsavl.so.0.7
$ ln -s libsavl.so.0.7.1 ${LIB_DIR}/libsavl.so
$ ls -l ${LIB_DIR}/libsavl* | awk '{ print $9 " " $10 " " $11 }'
/mnt/scratch/openwrt/staging_dir/target-mips_24kc_musl/usr/lib/libsavl.so -> libsavl.so.0.7.1
/mnt/scratch/openwrt/staging_dir/target-mips_24kc_musl/usr/lib/libsavl.so.0.7 -> libsavl.so.0.7.1
/mnt/scratch/openwrt/staging_dir/target-mips_24kc_musl/usr/lib/libsavl.so.0.7.1
Second, copy the header file to the header directory.
$ cp savl.h ${HEADER_DIR}
$ ls ${HEADER_DIR}/savl*
/mnt/scratch/openwrt/staging_dir/target-mips_24kc_musl/usr/include/savl.h
Return to the build directory, download the FDF repository, and change to its
src
subdirectory.
$ cd ${BUILD_DIR}
$ git clone https://github.com/ipilcher/fdf.git
Cloning into 'fdf'...
remote: Enumerating objects: 201, done.
remote: Counting objects: 100% (201/201), done.
remote: Compressing objects: 100% (141/141), done.
remote: Total 201 (delta 96), reused 160 (delta 57), pack-reused 0
Receiving objects: 100% (201/201), 89.44 KiB | 1.94 MiB/s, done.
Resolving deltas: 100% (96/96), done.
$ cd fdf/src
Update the API version and use the cross compiler to build the daemon (fdfd
).
$ ./apiver.sh
$ $XGCC -O3 -Wall -Wextra -Wcast-align -o fdfd -I${HEADER_DIR} *.c \
-L${LIB_DIR} -lsavl -ljson-c -ldl -Wl,--dynamic-list=symlist
$ file fdfd
fdfd: ELF 32-bit MSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter
/lib/ld-musl-mips-sf.so.1, with debug_info, not stripped
Change to the repository's src/filters
subdirectory and use the cross
compiler to build the included filters.
$ cd filters
$ $XGCC -O3 -Wall -Wextra -Wcast-align -shared -fPIC -o mdns.so \
-I${HEADER_DIR} -I.. mdns.c -L${LIB_DIR} -lsavl
$ $XGCC -O3 -Wall -Wextra -Wcast-align -shared -fPIC -o ipset.so \
-I${HEADER_DIR} -I.. ipset.c -L${LIB_DIR} -lmnl
$ file *.so
ipset.so: ELF 32-bit MSB shared object, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, with
debug_info, not stripped
mdns.so: ELF 32-bit MSB shared object, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, with
debug_info, not stripped
Return to the build directory and copy the required files to the OpenWrt device
(referred to as ${OPENWRT_DEV}
below).
$ cd ${BUILD_DIR}
$ scp libsavl/libsavl.so.* fdf/src/fdfd fdf/src/filters/*.so fdf/openwrt/* root@${OPENWRT_DEV}:
root@wap2's password:
libsavl.so.0.7.1 100% 11KB 227.6KB/s 00:00
fdfd 100% 41KB 240.9KB/s 00:00
ipset.so 100% 13KB 226.8KB/s 00:00
mdns.so 100% 17KB 232.9KB/s 00:00
fdf 100% 142 30.8KB/s 00:00
fdfd 100% 1037 139.2KB/s 00:00
fdfd.json 100% 277 54.8KB/s 00:00
fdf-ipsets 100% 371 69.6KB/s 00:00
On the OpenWrt device, copy the shared object file to the library directory, and create a symbolic link that reflects its soname.
NOTE: If necessary, use the
objdump
utility on the build system to determine the library's soname.$ objdump -p libsavl/libsavl.so.* | grep SONAME SONAME libsavl.so.0.7
# ls libsavl.so.*
libsavl.so.0.7.1
# cp libsavl.so.0.7.1 /usr/lib/
# ln -s libsavl.so.0.7.1 /usr/lib/libsavl.so.0.7
Use opkg
to ensure that the JSON-C (libjson-c5
) and libmnl
(libmnl0
)
libraries and the ipset
tool are all installed. Installation of the
procd-ujail
package is also recommended, because it will enable FDF to run
as a non-root
user, with limited privileges.
NOTE: The
libmnl0
andipset
packages are not required if the IP set filter will not be used.
# for PKG in libjson-c5 libmnl0 ipset procd-ujail ; do opkg status $PKG ; done | grep ^Status
Status: install user installed
Status: install user installed
Status: install user installed
Status: install user installed
On the OpenWrt device, copy the FDF daemon executable to /usr/bin
, create a
directory for FDF filter shared objects, and copy the filters to that directory.
# cp fdfd /usr/bin/
# mkdir /usr/lib/fdf-filters
# cp mdns.so ipset.so /usr/lib/fdf-filters/
NOTE: The
fdf-ipsets
initialization script is not required if the IP set filter will not be used.
# cp fdfd fdf-ipsets /etc/init.d/
# cp fdfd.json /etc/capabilities/
Follow the directions here to create the FDF
configuration (/etc/fdf-config.json
).
Additionally, create the /etc/config/fdf
UCI configuration, which
controls the behavior of the initialization scripts (/etc/init.d/fdfd
and
/etc/init.d/fdf-ipsets
). This repository contains a sample UCI configuration
file (openwrt/fdf
) that can be used as a reference.
As an alternative to editing the file, the uci
command can be used to display
and modify the configuration. Consider the sample configuration.
# cat /etc/config/fdf
config startup daemon
# All network interfaces used by FDF
option interfaces 'eth0 eth1'
# (Optional) fdfd command-line options
option options '-d -p'
# (Optional) IP sets to be created by the fdf-ipsets service
config ipset SET1
config ipset SET2
# uci show fdf
fdf.daemon=startup
fdf.daemon.interfaces='eth0 eth1'
fdf.daemon.options='-d -p'
fdf.SET1=ipset
fdf.SET2=ipset
-
fdf.daemon.interfaces
is required. It should be a whitespace separated list (enclosed in double or single quotes) of all network interfaces that FDF will use, as either a source or destination, when forwarding packets. Thefdfd
initialization script will wait until these interfaces exist before starting the FDF daemon. -
fdf.daemon.options
is optional. It can be use to pass command-line options to the FDF daemon. See Runningfdfd
. -
fdf.SET1
andfdf.SET2
specify IP sets that will be created by thefdf-ipsets
initialization script. (Technically, each is an empty UCI section of typeipset
. A future version of thefdf-ipsets
script may use options within these sections to create IP sets with specific options.) Any IP set used by the IP set filter should have such a section defined.
As mentioned above, I do not use OpenWrt as a router, only as
"layer 2" wireless access point. The device does not have an IP address on any
of the subnets used by my wireless networks. Its only IP address is on my
"management" subnet, on an isolated VLAN. Thus, it needs only a very simple
firewall configuration. Because I am familiar with the "legacy" iptables
syntax used on Red Hat, Fedora, and similar distributions before the adoption of
firewalld
, I use a
simple initialization script
that allows me to use this syntax on OpenWrt.
For this reason, I cannot provide detailed instructions for configuring the OpenWrt firewall, only general principles.
-
The device must have an IPv4 address configured on any network interface that FDF will use to receive packets. (If the device is being used as a router, this will usually already be true.)
-
The device firewall must be configured to allow incoming traffic that matches the FDF listeners. For example, if FDF is listening for multicast DNS packets on
eth0
, which is connected to the192.168.5.0/24
subnet, then the firewall must be configured to allow traffic sent to IP address224.0.0.251
, UDP port5353
, from that network and subnet to be received. -
Unlike multicast DNS (usually), most network discovery protocols use unicast response packets. FDF will not forward these response packets; the network router (usually the OpenWrt device) must be configured to route them to their destination (along with any "post-discovery" traffic).
When the OpenWrt device is also the router (the most common setup), the device firewall must be configured to allow the routed traffic. This can be achieved with static rules or with the FDF IP set filter.
It is recommended to first run FDF manually, with all debugging enabled.
# fdfd -d -p
DEBUG: config.c:387: Parsing filter (/filters/mdns_query)
DEBUG: mdns_query: mDNS filter mode set to STATEFUL
DEBUG: mdns_query: Instance set to IPSET mode
⋮
After confirming that FDF is working as expected, terminate the program with
Ctrl-C
(SIGINT
).
Run FDF as a service, to ensure that the initialization script works.
# service fdfd start
# service fdfd status
running
Finally, enable the required services — fdfd
and (optionally)
fdf-ipsets
and reboot the device.
# service fdfd enable
# service fdf-ipsets enable
# reboot
When the device has finished booting, verify that any required IP sets were
created (ipset list
) and the FDF daemon is running as expected.