# Technical Documentation: openwrt/packages

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/openwrt/packages)

### Overview (`README.md`)

# OpenWrt packages feed

## Description

This is the OpenWrt "packages"-feed containing community-maintained build scripts, options and patches for applications, modules and libraries used within OpenWrt.

Installation of pre-built packages is handled directly by the **opkg** utility within your running OpenWrt system or by using the [OpenWrt SDK](https://openwrt.org/docs/guide-developer/using_the_sdk) on a build system.

## Usage

This repository is intended to be layered on-top of an OpenWrt buildroot. If you do not have an OpenWrt buildroot installed, see the documentation at: [OpenWrt Buildroot – Installation](https://openwrt.org/docs/guide-developer/build-system/install-buildsystem) on the OpenWrt support site.

This feed is enabled by default. To install all its package definitions, run:
```
./scripts/feeds update packages
./scripts/feeds install -a -p packages
```

## License

See [LICENSE](LICENSE) file.
 
## Package Guidelines

See [CONTRIBUTING.md](CONTRIBUTING.md) file.

---

### Overview (`lang/python/README.md`)

# Python packages folder

## Table of contents

1. [Description](#description)
2. [Introduction](#introduction)
3. [Python 2 end-of-life](#python-2-end-of-life)
4. [Using Python in external/other package feeds](#using-python-in-externalother-package-feeds)
5. [Build considerations](#build-considerations)
6. [General folder structure](#general-folder-structure)
7. [Building a Python package](#building-a-python-package)
    1. [Include python3-package.mk](#include-python3-packagemk)
    2. [Add Package/<PKG_NAME> OpenWrt definitions](#add-packagepkg_name-openwrt-definitions)
    3. [Python package dependencies](#python-package-dependencies)
    4. [Wrapping things up so that they build](#wrapping-things-up-so-that-they-build)
    5. [Customizing things](#customizing-things)
    6. [Host-side Python packages for build](#host-side-python-packages-for-build)

## Description

This section describes specifics for the Python packages that are present in this repo, and how things are structured.

In terms of license, contributing guide, etc, all of that information is described in the top [README.md](../../README.md) file, and it applies here as well. This document attempts to cover only technical aspects of Python packages, and maybe some explanations about how things are (and why they are as they are).

## Introduction

This sub-tree came to exist after a number of contributions (Python packages) were made to this repo, and the [lang](../) subtree grew to a point where a decision was made to move all Python packages under [lang/python](./).

It contains the Python 3 interpreter and Python packages. Most of the Python packages are downloaded from [pypi.org](https://pypi.org/). Python packages from pypi.org are typically preferred when adding new packages.

If more packages (than the ones packaged here) are needed, they can be downloaded via [pip](https://pip.pypa.io). Note that the versions of `pip` & `setuptools` [available in this repo] are the ones that are packaged inside the Python package (yes, Python comes packaged with `pip` & `setuptools`).

## Python 2 end-of-life

Python 2 [will not be maintained past 2020](https://www.python.org/dev/peps/pep-0373/). All Python 2 packages have been removed from the packages feed (this repo) and archived in the [abandoned packages feed](https://github.com/openwrt/packages-abandoned).

## Using Python in external/other package feeds

In the feeds.conf (or feeds.conf.default file, whatever is preferred), the packages repo should be present.

Example
```
src-git packages https://git.openwrt.org/feed/packages.git
src-git luci https://git.openwrt.org/project/luci.git
src-git routing https://git.openwrt.org/feed/routing.git
src-git telephony https://git.openwrt.org/feed/telephony.git
#
#
src-git someotherfeed https://github.com/<github-user>/<some-other-package>
```

Assuming that there are Python packages in the `<some-other-package>`, they should include `python3-package.mk` like this:
```
include $(TOPDIR)/feeds/packages/lang/python/python3-package.mk
```

Same rules apply for `python3-package.mk` as the Python packages in this repo.

**One important consideration:**: if the local name is not `packages`, it's something else, like `openwrt-packages`. And in `feeds.conf[.default]` it's:
```
src-git openwrt-packages https://git.openwrt.org/feed/packages.git
```

Then, the inclusions also change:
```
include $(TOPDIR)/feeds/openwrt-packages/lang/python/python3-package.mk
```

Each maintainer[s] of external packages feeds is responsible for the local name, and relative inclusion path back to this feed (which is named `packages` by default).

In case there is a need/requirement such that the local package feed is named something else than `packages`, one approach to make the package flexible to change is:

```
PYTHON3_PACKAGE_MK:=$(wildcard $(TOPDIR)/feeds/*/lang/python/python3-package.mk)

# verify that there is only one single file returned
ifneq (1,$(words $(PYTHON3_PACKAGE_MK)))
ifeq (0,$(words $(PYTHON3_PACKAGE_MK)))
$(error did not find python3-package.mk in any feed)
else
$(error found multiple python3-package.mk files in the feeds)
endif
else
$(info found python3-package.mk at $(PYTHON3_PACKAGE_MK))
endif

include $(PYTHON3_PACKAGE_MK)
```

This should solve the corner-case where the `python3-package.mk` can be in some other feed, or if the packages feed will be named something else locally.

## Build considerations

In order to build the Python interpreter, a host Python interpreter needs to be built, in order to process some of the build for the target Python build. The host Python interpreter is also needed so that Python bytecodes are generated, so the host interpreter needs to be the exact version as on the target. And finally, the host Python interpreter also provides pip, so that it may be used to install some Python packages that are required to build other Python packages.
That's why you'll also see a Python build & staging directories.

As you're probably thinking, this sounds [and is] somewhat too much complication [just for packaging], but the status of things is-as-it-is, and it's probably much worse than what's currently visible on the surface [with respect to packaging Python & packages].

As mentioned earlier, Python packages are shipped with bytecodes, and the reason for this is simply performance & size.
The thought/discussion matrix derives a bit like this:
* shipping both Python source-code & bytecodes takes too much space on some devices ; Python source code & byte-code take about similar disk-size
* shipping only Python source code has a big performance penalty [on some lower end systems] ; something like 500 msecs (Python source-only) -> 70 msecs (Python byte-codes) time reduction for a simple "Hello World" script
* shipping only Python byte-codes seems like a good trade-off, and this means that `python3-src` can be provided for people that want the source code

By default, automatic Python byte-code generation is disabled when running a Python script, in order to prevent a disk from accidentally filling up. Since some disks reside in RAM, this also means not filling up the RAM. If someone wants to convert Python source to byte-code then he/she is free to compile it [directly on the device] manually via the Python interpreter & library.

## General folder structure

The basis of all these packages is:
* [lang/python/python3](./python3) - The Python 3.x.y interpreter

This is a normal OpenWrt package, which will build the Python interpreter. This also provides `python3-pip` & `python3-setuptools`. Each Python package is actually split into multiple sub-packages [e.g. python3-urllib, python3-sqlite3, etc]. This can be viewed inside [lang/python/python3/files](./python3/files).

The reason for this splitting, is purely to offer a way for some people to package Python in as-minimal-as-possible-and-still-runable way, and also to be somewhat maintainable when packaging. A standard Python installation can take ~20-30 MBs of disk, which can be somewhat big for some people, so there is the `python3-base` package which brings that down to ~5 MBs. This seems to be good enough (and interesting) for a number of people.

The Python interpreter is structured like this:
* `python3-base`, which is just the minimal package to startup Python and run basic commands
* `python3` is a meta-package, which installs almost everything (python3-base [plus] Python library [minus] some unit-tests & some windows-y things)
* `python3-light` is `python3` [minus] packages that are in [lang/python/python3/files](./python3/files) ; the size of this package may be sensible (and interesting) to another group of people

All other Python packages (aside from the intepreter) typically use these files:
* **python3-host.mk** - this file contains paths and build rules for running the Python interpreter on the host-side; they also provide paths to host interprete, host Python lib-dir & so on
* **python3-package.mk**
  * includes **python3-host.mk**
  * contains all the default build rules for Python packages; these will be detailed below in the [Building a Python package](#building-a-python-package) section

**Note** that Python packages don't need to use these files (i.e. `python3-package.mk` & `python3-host.mk`), but they do provide some ease-of-use & reduction of duplicate code. And they do contain some learned-lessons about packaging Python packages, so it's a good idea to use them.

## Building a Python package

### Include python3-package.mk

Add this after  `include $(INCLUDE_DIR)/package.mk`
```
include ../python3-package.mk
```

This will make sure that build rules for Python can be specified and picked up for build.

### Include pypi.mk (optional)

`pypi.mk` is an include file that makes downloading package source code from [pypi.org](https://pypi.org/) simpler.

To use `pypi.mk`, add this **before** `include $(INCLUDE_DIR)/package.mk`:
```
include ../pypi.mk
```

`pypi.mk` has several `PYPI_*` variables that can/must be set (see below); these should be set before `pypi.mk` is included, i.e. before the `include ../pypi.mk` line.

`pypi.mk` also provides default values for `PKG_SOURCE` and `PKG_SOURCE_URL`, so these variables may be omitted.

Required variables:

* `PYPI_NAME`: Package name on pypi.org. This should match the PyPI name exactly.

  For example (from the `python-yaml` package):
  ```
  PYPI_NAME:=PyYAML
  ```

Optional variables:

* `PYPI_SOURCE_NAME`: Package name component of the source tarball filename  
  Default: Same value as `PYPI_NAME`

* `PYPI_SOURCE_EXT`: File extension of the source tarball filename  
  Default: `tar.gz`

`pypi.mk` constructs the default `PKG_SOURCE` value from these variables (and `PKG_VERSION`):
```
PKG_SOURCE?=$(PYPI_SOURCE_NAME)-$(PKG_VERSION).$(PYPI_SOURCE_EXT)
```

### Add Package/<PKG_NAME> OpenWrt definitions

This part is similar to default OpenWrt packages.

Example:
```
define Package/python3-lxml
  SECTION:=lang
  CATEGORY:=Languages
  SUBMENU:=Python
  TITLE:=Pythonic XML processing library
  URL:=https://lxml.de
  DEPENDS:=+python3-light +libxml2 +libxslt +libexslt
endef

define Package/python3-lxml/description
  The lxml XML toolkit is a Pythonic binding
  for the C libraries libxml2 and libxslt.
endef
```

Some considerations here (based on the example above):
* typically the package is named `Package/python3-<something>` ; this convention makes things easier to follow, though it could work without naming things this way
* `TITLE` can be something a bit more verbose/neat ; typically the name is short as seen above

### Python package dependencies

Aside from other libraries and programs, every Python package will depend on at least one of these three types of packages:

* The Python interpreter: All Python packages should depend on one of these three interpreter packages:

  * `python3-light` is the best default for most Python packages.

  * `python3-base` should only be used as a dependency if you are certain the bare interpreter is sufficient.

  * `python3` is useful if many (more than three) Python standard library packages are needed.

* Python standard library packages: As noted above, many parts of the Python standard library are packaged separate from the Python interpreter. These packages are defined by the files in [lang/python/python3/files](./python3/files).

  To find out which of these separate standard library packages are necessary, after completing a draft Makefile (including the `$(eval ...)` lines described in the next section), run `make` with the `configure` target and `PY3=stdlib V=s` in the command line. For example:

  ```
  make package/python-lxml/configure PY3=stdlib V=s
  ```

  If the package has been built previously, include the `clean` target to trigger configure again:

  ```
  make package/python-lxml/{clean,configure} PY3=stdlib V=s
  ```

  This will search the package for module imports and generate a list of suggested dependencies. Some of the found imports may be false positives, e.g. in example or test files, so examine the matches for more information.

* Other Python packages: The easiest way to find these dependencies is to look for the `install_requires` keyword inside the package's `setup.py` file (it will be a keyword argument to the `setup()` function). This will be a list of run-time dependencies for the package.

  There may already be packages in the packages feed that provide these dependencies. If not, they will need to be packaged for your Python package to function correctly.

  Any packages in a `setup_requires` keyword argument are build-time dependencies that may need to be installed on the host (host Python inside of OpenWrt buildroot, not system Python that is part of the outer computer system). To ensure these build-time dependencies are present, see [Host-side Python packages for build](#host-side-python-packages-for-build). (Note that Setuptools is already installed as part of host Python.)

### Wrapping things up so that they build

If all the above prerequisites have been met, all that's left is:

```
$(eval $(call Py3Package,python3-lxml))
$(eval $(call BuildPackage,python3-lxml))
```

The `$(eval $(call Py3Package,python3-lxml))` part will instantiate all the default Python build rules so that the final Python package is packaged into an OpenWrt.
And `$(eval $(call BuildPackage,python3-lxml))` will bind all the rules generated with `$(eval $(call Py3Package,python3-lxml))` into the OpenWrt build system.

These packages will contain byte-codes and binaries (shared libs & other stuff).

If a user wishes to ship source code, adding one more line creates one more package that ship Python source code:
```
$(eval $(call Py3Package,python3-lxml))
$(eval $(call BuildPackage,python3-lxml))
$(eval $(call BuildPackage,python3-lxml-src))
```

The name `*-src` must be the Python package name; so for `python3-lxml-src` a equivalent `python3-lxml` name must exist.

### Customizing things

Some packages need custom build rules (because they do).

The default package build and install processes are defined in `python3-package.mk`.

#### Building

The default build process calls `setup.py install` inside the directory where the Python source package is extracted (`PKG_BUILD_DIR`). This "installs" the Python package to an intermediate location (`PKG_INSTALL_DIR`) where it is used by the default install process.

There are several Makefile variables that can be used to customize this process (all optional):

* `PYTHON3_PKG_SETUP_DIR`: Path where `setup.py` can be found, relative to the package directory (`PKG_BUILD_DIR`).  
  Default: empty value (`setup.py` is in the package directory)
* `PYTHON3_PKG_SETUP_VARS`: Additional environment variables to set for the call to `setup.py`. Should be in the form of `VARIABLE1=value VARIABLE2=value ...`.  
  Default: empty value
* `PYTHON3_PKG_SETUP_GLOBAL_ARGS`: Additional command line arguments to pass to `setup.py`, before / in front of the `install` command.  
  Default: empty value
* `PYTHON3_PKG_SETUP_ARGS`: Additional command line arguments to pass to `setup.py`, after the `install` command.  
  Default: `--single-version-externally-managed`

Conceptually, these variables are used in this way:

```
cd $(PKG_BUILD_DIR)/$(PYTHON3_PKG_SETUP_DIR)
$(PYTHON3_PKG_SETUP_VARS) python3 setup.py $(PYTHON3_PKG_SETUP_GLOBAL_ARGS) install $(PYTHON3_PKG_SETUP_ARGS)
```

The default build process can be completely overridden by defining a custom `Py3Build/Compile` rule in the package Makefile.

#### Installing

The default install process copies some/all of the files from `PKG_INSTALL_DIR`, placed there by the build process, to a location passed to the install rule as the first argument (`$(1)`). The OpenWrt build system will then take those files and create the actual .ipk package archives.

This default process uses 2 build rules:
* `Py3Package/<package>/filespec` which are Python library files relative to `/usr/lib/pythonX.Y` ; by default this is `/usr/lib/python$(PYTHON3_VERSION)/site-packages` (`PYTHON3_PKG_DIR`) ; most Python packages generate files that get installed in this sub-folder
* `Py3Package/<package>/install` is similar to `Package/<package>/install` ; this allows binary (or other files) to be installed on the target

Both the 2 above rules generate a `Package/<package>/install` build rule, which gets picked up by the build system. Both can be used together (they are not mutually exclusive), and provide a good enough flexibility for specifying Python packages.

The `Py3Package/<package>/filespec` rule contains one or more lines of the following format (whitespace added for clarity):

```
<one of: +-=> | <file/directory path> | <file permissions>
```

The initial character controls the action that will be taken:

* `+`: Install the given path. If the path is a directory, all files and subdirectories inside are installed.
  * If file permissions is specified (optional), then the file or directory (and all files and subdirectories) are assigned the given permissions; if omitted, then the file or directory retains its original permissions.
* `-`: Remove the given path. Useful when most of a directory should be installed except for a few files or subdirectories.
  * File permissions is not used / ignored in this case.
* `=`: Assign the given file permissions to the given path. File permissions is required in this case.

As mentioned, the default `Py3Package/<package>/filespec` installs `PYTHON3_PKG_DIR`:

```
define Py3Package/python3-example/filespec
+|$(PYTHON3_PKG_DIR)
endef
```

If the package installs a `example_package` directory inside `PYTHON3_PKG_DIR`, and there is an `examples` directory and `test_*.py` files that can be omitted to save space, this can be specified as:

```
define Py3Package/python3-example/filespec
+|$(PYTHON3_PKG_DIR)
-|$(PYTHON3_PKG_DIR)/example_package/examples
-|$(PYTHON3_PKG_DIR)/example_package/test_*.py
endef
```

### Host-side Python packages for build

These can be installed via pip and ideally they should only be installed like this, because it's a bit simpler than running them through the OpenWrt build system.

#### Requirements files

All host-side Python packages are installed with pip using [requirements files](https://pip.pypa.io/en/stable/user_guide/#requirements-files), with [hash-checking mode](https://pip.pypa.io/en/stable/reference/pip_install/#hash-checking-mode) enabled. These requirements files are stored in the [host-pip-requirements](./host-pip-requirements) directory.

Each requirements file is named after the Python package it installs and contains the package's pinned version and `--hash` option. The `--hash` option value is the SHA256 hash of the package's source tarball; this value can be found on [pypi.org](https://pypi.org/).

For example, the requirements file for setuptools-scm ([setuptools-scm.txt](./host-pip-requirements/setuptools-scm.txt)) contains:

```
setuptools-scm==4.1.2 --hash=sha256:a8994582e716ec690f33fec70cca0f85bd23ec974e3f783233e4879090a7faa8
```

If the Python package to be installed depends on other Python packages, those dependencies, with their pinned versions and `--hash` options, also need to be specified in the requirements file. For instance, [cffi.txt](./host-pip-requirements/cffi.txt) includes information for pycparser because pycparser is a dependency of cffi and will be installed with cffi.

There are two types of requirements files in [host-pip-requirements](./host-pip-requirements):

* Installs the latest version of a Python package.

  A requirements file of this type is named with the package name only (for example, [setuptools-scm.txt](./host-pip-requirements/setuptools-scm.txt)) and is used when there is no strict version requirement.

  These files will be updated as newer versions of the Python packages are available.

* Installs a specific version of a Python package.

  A requirements file of this type is named with the package name and version number (for example, [Django-1.11.txt](./host-pip-requirements/Django-1.11.txt)) and is used when a specific (usually older) version is required.

  Installing the latest versions of packages is preferred over specific versions whenever possible.

#### Installing host-side Python packages

Set `HOST_PYTHON3_PACKAGE_BUILD_DEPENDS` to the names of one or more requirements files in [host-pip-requirements](./host-pip-requirements), without the directory path or ".txt" extension.

For example:

```
HOST_PYTHON3_PACKAGE_BUILD_DEPENDS:=setuptools-scm
```

The Python package will be installed in `$(STAGING_DIR_HOSTPKG)/lib/pythonX.Y/site-packages`.

#### Non-Python packages installing host-side Python packages

Non-Python packages can also install host-side Python packages using the same mechanism:

* Set `HOST_PYTHON3_PACKAGE_BUILD_DEPENDS` (see above for details).

* Include `python3-package.mk` (set `PYTHON3_PKG_BUILD:=0` to avoid using the default Python package build recipes).

* Call `Py3Build/InstallBuildDepends` to initiate the installation.

For example:

```
PYTHON3_PKG_BUILD:=0
HOST_PYTHON3_PACKAGE_BUILD_DEPENDS:=setuptools-scm

include $(INCLUDE_DIR)/package.mk
include ../../lang/python/python3-package.mk
# If outside of the packages feed:
# include $(TOPDIR)/feeds/packages/lang/python/python3-package.mk

define Build/Compile
	$(call Py3Build/InstallBuildDepends)
	$(call Build/Compile/Default)
endef
```

---

### Overview (`net/adblock-fast/README.md`)

# adblock-fast

[](https://openwrt.org)
[](https://docs.mossdef.org/adblock-fast/)
[](https://openwrt.org/packages/pkgdata/adblock-fast)
[](https://github.com/stangri/adblock-fast/blob/master/LICENSE)

A fast, lightweight DNS-based ad-blocker for OpenWrt that works with dnsmasq, smartdns, or unbound.  
It runs once to process and install blocklists, then exits — keeping memory usage low.

## Features

- Minimal runtime memory use
- Parallel blocklist download and processing
- Persistent cache support
- Optional Web UI for custom block/allow lists
- Reverts if DNS resolution fails after restart

📚 **Full documentation:**  
[https://docs.mossdef.org/adblock-fast/](https://docs.mossdef.org/adblock-fast/)

---

### Overview (`net/adblock/README.md`)

# DNS based ad/abuse domain blocking

## Table of Contents
* [Description](#description)
* [Quick Start](#quick-start)
* [Main Features](#main-features)
* [Prerequisites](#prerequisites)
* [Installation & Usage](#installation-and-usage)
* [Adblock CLI interface](#adblock-cli-interface)
* [Adblock Config Options](#adblock-config-options)
* [Examples](#examples)
* [Best practice and tweaks](#best-practice-and-tweaks)
* [Troubleshooting & debug options](#troubleshooting-and-debug-options)
* [Support](#support)
* [Removal](#removal)
* [Donations](#donations)

<a id="description"></a>
## Description
A lot of people already use adblocker plugins within their desktop browsers, but what if you are using your (smart) phone, tablet, watch or any other (wlan) gadget!? Getting rid of annoying ads, trackers and other abuse sites (like facebook) is simple: block them with your router.

When the DNS server on your router receives DNS requests, you will sort out queries that ask for the resource records of ad servers and return a simple `NXDOMAIN`. This is nothing but **N**on-e**X**istent Internet or Intranet domain name, if a domain name cannot be resolved using the DNS server, a condition called the `NXDOMAIN` occurred.

<a id="quick-start"></a>
## Quick Start
For a typical setup these few steps are enough to get adblock up and running — see the sections below for details:
1. Install the LuCI companion package: `apk update && apk add luci-app-adblock` (this pulls in the `adblock` backend as a dependency).
2. Enable the adblock system service under `System → Startup`, then open LuCI under `Services → Adblock`, tick `Enabled` and (recommended) set a `Startup Trigger Interface` to your WAN interface(s).
3. Keep the small, pre-selected default feed selection to start with (e.g. `adguard`, `adguard_tracking` and `certpl`, ≈280K domains).
4. Start and verify the service:

```sh
/etc/init.d/adblock start
/etc/init.d/adblock status
```

**Please note:** don't blindly enable (too) many feeds at once — on low memory devices this will sooner or later lead to OOM conditions.

<a id="main-features"></a>
## Main Features
Support of the following fully pre-configured domain blocklist feeds (free for private usage, for commercial use please check their individual licenses)

| Feed                | Enabled | Size | Focus            | Information                                                                       |
| :------------------ | :-----: | :--- | :--------------- | :-------------------------------------------------------------------------------- |
| 1Hosts              |         | VAR  | compilation      | [Link](https://github.com/badmojr/1Hosts)                                         |
| adguard             |    x    | L    | general          | [Link](https://adguard.com)                                                       |
| adguard_tracking    |    x    | L    | tracking         | [Link](https://github.com/AdguardTeam/cname-trackers)                             |
| android_tracking    |         | S    | tracking         | [Link](https://github.com/Perflyst/PiHoleBlocklist)                               |
| andryou             |         | L    | compilation      | [Link](https://gitlab.com/andryou/block/-/blob/master/readme.md)                  |
| anti_ad             |         | L    | compilation      | [Link](https://github.com/privacy-protection-tools/anti-AD/blob/master/README.md) |
| anudeep             |         | M    | compilation      | [Link](https://github.com/anudeepND/blacklist)                                    |
| bitcoin             |         | S    | mining           | [Link](https://github.com/hoshsadiq/adblock-nocoin-list)                          |
| certpl              |    x    | L    | phishing         | [Link](https://cert.pl/en/warning-list/)                                          |
| cpbl                |         | XL   | compilation      | [Link](https://github.com/bongochong/CombinedPrivacyBlockLists)                   |
| disconnect          |         | S    | general          | [Link](https://disconnect.me)                                                     |
| divested            |         | XXL  | compilation      | [Link](https://divested.dev/pages/dnsbl)                                          |
| doh_blocklist       |         | S    | doh_server       | [Link](https://github.com/dibdot/DoH-IP-blocklists)                               |
| firetv_tracking     |         | S    | tracking         | [Link](https://github.com/Perflyst/PiHoleBlocklist)                               |
| games_tracking      |         | S    | tracking         | [Link](https://www.gameindustry.eu)                                               |
| hagezi              |         | VAR  | compilation      | [Link](https://github.com/hagezi/dns-blocklists)                                  |
| hblock              |         | XL   | compilation      | [Link](https://hblock.molinero.dev)                                               |
| ipfire_dbl          |         | VAR  | compilation      | [Link](https://www.ipfire.org/dbl)                                                |
| oisd_big            |         | XXL  | general          | [Link](https://oisd.nl)                                                           |
| oisd_nsfw           |         | XXL  | porn             | [Link](https://oisd.nl)                                                           |
| oisd_nsfw_small     |         | M    | porn             | [Link](https://oisd.nl)                                                           |
| oisd_small          |         | L    | general          | [Link](https://oisd.nl)                                                           |
| phishing_army       |         | S    | phishing         | [Link](https://phishing.army)                                                     |
| smarttv_tracking    |         | S    | tracking         | [Link](https://github.com/Perflyst/PiHoleBlocklist)                               |
| spam404             |         | S    | general          | [Link](https://github.com/Dawsey21)                                               |
| stevenblack         |         | VAR  | compilation      | [Link](https://github.com/StevenBlack/hosts)                                      |
| stopforumspam       |         | S    | spam             | [Link](https://www.stopforumspam.com)                                             |
| utcapitole          |         | VAR  | general          | [Link](https://dsi.ut-capitole.fr/blacklists/index_en.php)                        |
| wally3k             |         | S    | compilation      | [Link](https://firebog.net/about)                                                 |
| whocares            |         | M    | general          | [Link](https://someonewhocares.org)                                               |
| winspy              |         | S    | win_telemetry    | [Link](https://github.com/crazy-max/WindowsSpyBlocker)                            |
| yoyo                |         | S    | general          | [Link](https://pgl.yoyo.org/adservers)                                            |

**Please note:** Feeds marked with size **VAR** (`1Hosts`, `hagezi`, `ipfire_dbl`, `stevenblack`, `utcapitole`) additionally require a category selection via the options `adb_hst_feed`, `adb_hag_feed`, `adb_ipf_feed`, `adb_stb_feed` and `adb_utc_feed` (or via the LuCI feed configuration). Without a category the feed is skipped during processing.  

* List of supported and fully pre-configured adblock sources, already active sources are pre-selected.
  <b><em>To avoid OOM errors, please do not select too many lists!</em></b>
  List size information with the respective domain ranges as follows:
    • <b>S</b> (-10k), <b>M</b> (10k-30k) and <b>L</b> (30k-80k) should work for 128 MByte devices
    • <b>XL</b> (80k-200k) should work for 256-512 MByte devices
    • <b>XXL</b> (200k-) needs more RAM and Multicore support, e.g. x86 or raspberry devices
    • <b>VAR</b> (50k-900k) variable size depending on the selection
* Zero-conf like automatic installation & setup, usually no manual changes needed
* Simple but yet powerful adblock engine: adblock does not use error prone external iptables rulesets, http pixel server instances and things like that
* Supports six different DNS backend formats: dnsmasq, unbound, named (bind), kresd, smartdns or raw (e.g. used by dnscrypt-proxy)
* Supports three different SSL-enabled download utilities: uclient-fetch, full wget or curl
* Supports SafeSearch for google, bing, brave, duckduckgo, yandex, youtube and pixabay
* Fast downloads & list processing as they are handled in parallel running background jobs with multicore support
* The download engine supports ETAG headers to download only updated feeds
* Supports a wide range of router modes, even AP modes are supported
* Full IPv4 and IPv6 support
* Provides top level domain compression (`tld compression`), this feature removes thousands of needless host entries from the blocklist and lowers the memory footprint for the DNS backend
* Packs multiple domains per dnsmasq 'local=' line (max. 1000 characters), this reduces the size of the final blocklist by ~30% and speeds up the dnsmasq start
* Provides a `DNS Blocklist Shift`, where the generated final DNS blocklist is moved to the backup directory and only a soft link to this file is set in memory. As long as your backup directory is located on an external drive, you should activate this option to save valuable RAM.
* Feed parsing by a very fast & secure domain validator, all domain rules and feed information are placed in an external JSON file (`/etc/adblock/adblock.feeds`)
* Overall duplicate removal in generated blocklist file `adb_list.overall`
* Additional local allowlist for manual overrides, located in `/etc/adblock/adblock.allowlist`
* Additional local blocklist for manual overrides, located in `/etc/adblock/adblock.blocklist`
* Implements firewall‑based DNS Control to force DNS interfaces/ports and to redirect to external unfiltered/filtered DNS server
* Includes firewall‑based Remote DNS Allow, a CGI-Interface to allow certain MACs temporary bypass the local adblock DNS
* Supports firewall‑based temporary DNS Bridging, to ensure a Zero‑Downtime during adblock-related DNS Restarts
* Connection checks during blocklist update to ensure a reliable DNS backend service
* Minimal status & error logging to syslog, enable debug logging to receive more output
* Procd based init system support (`start`, `stop`, `restart`, `reload`, `enable`, `disable`, `running`, `status`, `suspend`, `resume`, `search`, `report`)
* Auto-Startup via procd network interface trigger or via classic time based startup
* Suspend & Resume adblock temporarily without blocklist re-processing
* Provides comprehensive runtime information
* Provides a detailed DNS Report with DNS related information about client requests, top (blocked) domains and more
* Provides a powerful search function to quickly find blocked (sub-)domains, e.g. to allow certain domains
* Implements a jail mode - only domains on the allowlist are permitted, all other DNS requests are rejected
* Automatic blocklist backup & restore: backups are used on `start`/`restart` and as a fallback on download errors — feeds are only actually refreshed via `reload`
* Send notification E-Mails, see example configuration below
* Add new adblock  feeds on your own with the `Custom Feed Editor` in LuCI or via CLI, see example below
* Strong LuCI support, all relevant options are exposed to the web frontend

<a id="prerequisites"></a>
## Prerequisites
* **[OpenWrt](https://openwrt.org)**, latest stable release or a development snapshot
* A usual setup with a working DNS backend
* A download utility with SSL support: `wget`, `uclient-fetch` with one of the `libustream-*` ssl libraries or `curl` is required
* A certificate store such as `ca-bundle` or `ca-certificates`, as adblock checks the validity of the SSL certificates of all download sites by default
* For E-Mail notifications you need to install and setup the additional `msmtp` package
* For DNS reporting you need to install the additional package `tcpdump-mini` or `tcpdump`

**Please note:**  
* Devices with less than 128MB of RAM are **_not_** supported
* For performance reasons, adblock depends on gnu sort and gawk
* Before update from former adblock releases please make a backup of your local allow- and blocklists. In the latest adblock these lists have been renamed to `/etc/adblock/adblock.allowlist` and `/etc/adblock/adblock.blocklist`. There is no automatic content transition to the new files.
* The uci configuration of adblock is automatically migrated during package installation via the uci-defaults mechanism using a housekeeping script
* Only `reload` actually refreshes the feeds (ETag check plus download of changed feeds). `start`, `restart` — and `boot`/`resume` — restore the existing blocklist backups and only download feeds that have **no** backup yet; they do **not** re-fetch already cached feeds. To update your blocklists (e.g. from a cron job) always use `reload`


<a id="installation-and-usage"></a>
## Installation & Usage
* Make a backup and update your local opkg/apk repository
* Install the LuCI companion package `luci-app-adblock` which also installs the main `adblock` package as a dependency
* Enable the adblock system service (System -> Startup) and enable adblock itself (adblock -> General Settings)
* It's strongly recommended to use the LuCI frontend to easily configure all aspects of adblock, the application is located in LuCI under the `Services` menu
* It's also recommended to configure a `Startup Trigger Interface` to depend on your WAN interface events during boot or restart of your router. Listing IPv6 interfaces (wan6) is fine as well: a trigger only starts a run if the last one did not succeed or if its blocklist is gone, so the chatty netifd update events no longer cause repeated downloads

<a id="adblock-cli-interface"></a>
## Adblock CLI interface
* The most important adblock functions are accessible via CLI as well.

```
~# /etc/init.d/adblock
Syntax: /etc/init.d/adblock [command]

Available commands:
	start           Start the service
	stop            Stop the service
	restart         Restart the service
	reload          Reload configuration files (or restart if service does not implement reload)
	enable          Enable service autostart
	disable         Disable service autostart
	enabled         Check if service is started on boot
	suspend         Suspend adblock processing
	resume          Resume adblock processing
	search          <domain> Search active blocklists and backups for a specific domain
	report          [<cli>|<mail>|<gen>|<json>] Print DNS statistics
	running         Check if service is running
	status          Service status
	trace           Start with syscall trace
	info            Dump procd service info
```

The `report` sub-command accepts an output mode: `cli` (default, human-readable table printed to the console), `json` (machine-readable output, incl. GeoIP map data when `adb_map=1`), `mail` (send the report via `msmtp`) and `gen` (regenerate the report data files in the background, used by the LuCI frontend).

<a id="adblock-config-options"></a>
## Adblock Config Options
* Usually the auto pre-configured adblock setup works quite well and no manual overrides are needed

| Option               | Default                            | Description/Valid Values                                                                           |
| :------------------- | :--------------------------------- | :------------------------------------------------------------------------------------------------- |
| adb_enabled          | 1, enabled                         | set to 0 to disable the adblock service                                                            |
| adb_feedfile         | /etc/adblock/adblock.feeds         | full path to the used adblock feed file                                                            |
| adb_dns              | -, auto-detected                   | `dnsmasq`, `unbound`, `named`, `kresd`, `smartdns` or `raw`                                        |
| adb_cores            | -, auto-detected                   | limit the cpu cores used by adblock; only auto-detection is memory-capped                          |
| adb_fetchcmd         | -, auto-detected                   | `uclient-fetch`, `wget` or `curl`                                                                  |
| adb_fetchparm        | -, auto-detected                   | manually override the config options for the selected download utility                             |
| adb_fetchretry       | 5                                  | number of download attempts in case of an error (not supported by uclient-fetch)                   |
| adb_fetchinsecure    | 0, disabled                        | don't check SSL server certificates during download                                                |
| adb_trigger          | -, not set                         | logical reload trigger interface(s), e.g. `wan` and `wan6`                                         |
| adb_triggerdelay     | 5                                  | additional trigger delay in seconds before adblock processing begins                               |
| adb_debug            | 0, disabled                        | set to 1 to enable the debug output                                                                |
| adb_nicelimit        | 0, standard prio.                  | valid nice level range 0-19 of the adblock processes                                               |
| adb_dnsshift         | 0, disabled                        | shift the blocklist to the backup directory and only set a soft link to this file in memory        |
| adb_dnsdir           | -, auto-detected                   | path for the generated blocklist file `adb_list.overall`                                           |
| adb_dnstimeout       | 20                                 | timeout in seconds to wait for a successful DNS backend restart                                    |
| adb_dnsinstance      | 0, first instance                  | set the relevant dnsmasq backend instance used by adblock                                          |
| adb_dnsflush         | 0, disabled                        | set to 1 to flush the DNS Cache before & after adblock processing                                  |
| adb_lookupdomain     | localhost                          | domain to check for a successful DNS backend restart                                               |
| adb_report           | 0, disabled                        | set to 1 to enable the background tcpdump gathering process for reporting                          |
| adb_map              | 0, disabled                        | enable a GeoIP Map with blocked domains                                                            |
| adb_reportdir        | /tmp/adblock-report                | path for DNS related report files                                                                  |
| adb_repiface         | -, auto-detected                   | name of the reporting interface or `any` used by tcpdump                                           |
| adb_repport          | 53                                 | list of reporting port(s) used by tcpdump                                                          |
| adb_repfilter        | -, not set                         | additional tcpdump filter expression, logically ANDed to the internal reporting port filter        |
| adb_repchunkcnt      | 5                                  | report chunk count used by tcpdump                                                                 |
| adb_repchunksize     | 1                                  | report chunk size used by tcpdump in MB                                                            |
| adb_represolve       | 0, disabled                        | resolve reporting IP addresses using reverse DNS (PTR) lookups                                     |
| adb_tld              | 1, enabled                         | set to 0 to disable the top level domain compression (tld) function                                |
| adb_basedir          | /tmp                               | path for all adblock related runtime operations, e.g. downloading, sorting, merging etc.           |
| adb_backupdir        | /tmp/adblock-backup                | path for adblock backups                                                                           |
| adb_safesearch       | 0, disabled                        | enforce SafeSearch for google, bing, brave, duckduckgo, yandex, youtube and pixabay                |
| adb_safesearchlist   | -, not set                         | limit SafeSearch to certain provider (see above)                                                   |
| adb_mail             | 0, disabled                        | set to 1 to enable notification E-Mails in case of a processing errors                             |
| adb_mailreceiver     | -, not set                         | receiver address for adblock notification E-Mails                                                  |
| adb_mailsender       | no-reply@adblock                   | sender address for adblock notification E-Mails                                                    |
| adb_mailtopic        | adblock notification               | topic for adblock notification E-Mails                                                             |
| adb_mailprofile      | adb_notify                         | mail profile used in `msmtp` for adblock notification E-Mails                                      |
| adb_jail             | 0                                  | jail mode - only domains on the allowlist are permitted, all other DNS requests are rejected       |
| adb_nftforce         | 0, disabled                        | redirect all local DNS queries from specified LAN zones to the local DNS resolver                  |
| adb_nftdevforce      | -, not set                         | firewall LAN Devices/VLANs that should be forced locally                                           |
| adb_nftportforce     | -, not set                         | firewall ports that should be forced locally                                                       |
| adb_nftallow         | 0, disabled                        | routes MACs or interfaces to an unfiltered external DNS resolver, bypassing local adblock          |
| adb_nftmacallow      | -, not set                         | listed MAC addresses will always use the configured unfiltered DNS server                          |
| adb_nftdevallow      | -, not set                         | entire interfaces or VLANs will be routed to the unfiltered DNS server                             |
| adb_allowdnsv4       | -, not set                         | external IPv4 DNS resolver applied to MACs and interfaces using the unfiltered DNS policy          |
| adb_allowdnsv6       | -, not set                         | external IPv6 DNS resolver applied to MACs and interfaces using the unfiltered DNS policy          |
| adb_nftremote        | 0, disabled                        | routes MACs to an unfiltered external DNS resolver, bypassing local adblock                        |
| adb_nftmacremote     | -, not set                         | Allows listed MACs to remotely access an unfiltered external DNS resolver, bypassing local adblock |
| adb_nftremotetimeout | 15                                 | Time limit in minutes for remote DNS access of the listed MAC addresses                            |
| adb_remotednsv4      | -, not set                         | external IPv4 DNS resolver applied to MACs using the unfiltered remote DNS policy                  |
| adb_remotednsv6      | -, not set                         | external IPv6 DNS resolver applied to MACs using the unfiltered remote DNS policy                  |
| adb_nftblock         | 0, disabled                        | routes MACs or interfaces to a filtered external DNS resolver, bypassing local adblock             |
| adb_nftmacblock      | -, not set                         | listed MAC addresses will always use the configured filtered DNS server                            |
| adb_nftdevblock      | -, not set                         | entire interfaces or VLANs will be routed to the filtered DNS server                               |
| adb_blockdnsv4       | -, not set                         | external IPv4 DNS resolver applied to MACs and interfaces using the filtered DNS policy            |
| adb_blockdnsv6       | -, not set                         | external IPv6 DNS resolver applied to MACs and interfaces using the filtered DNS policy            |
| adb_nftbridge        | -, not set                         | enables a temporary DNS bridge to an external DNS resolver during local DNS restarts               |
| adb_bridgednsv4	     | -, not set                         | external IPv4 DNS resolver used during bridging                                                    |
| adb_bridgednsv6	     | -, not set                         | external IPv6 DNS resolver used during bridging                                                    |
| adb_hst_feed         | -, not set                         | category selection for the `1hosts` feed (required to enable it)                                   |
| adb_hag_feed         | -, not set                         | category selection for the `hagezi` feed (required to enable it)                                   |
| adb_ipf_feed         | -, not set                         | category selection for the `ipfire_dbl` feed (required to enable it)                               |
| adb_stb_feed         | -, not set                         | category selection for the `stevenblack` feed (required to enable it)                              |
| adb_utc_feed         | -, not set                         | category selection for the `utcapitole` feed (required to enable it)                               |

<a id="examples"></a>
## Examples

**Change the DNS backend to `unbound`:**  
No further configuration is needed, adblock deposits the final blocklist `adb_list.overall` in `/var/lib/unbound` by default.
To preserve the DNS cache after adblock processing please install the additional package `unbound-control`.

**Change the DNS backend to `bind`:**  
Adblock deposits the final blocklist `adb_list.overall` in `/var/lib/bind` by default.
To preserve the DNS cache after adblock processing please install the additional package `bind-rndc`.
To use the blocklist please modify `/etc/bind/named.conf`:

```
in the `options` namespace add:
  response-policy { zone "rpz"; };

and at the end of the file add:
  zone "rpz" {
    type master;
    file "/var/lib/bind/adb_list.overall";
    allow-query { none; };
    allow-transfer { none; };
  };
```

**Change the DNS backend to `kresd`:**  
Adblock deposits the final blocklist `adb_list.overall` in `/tmp/kresd`, no further configuration needed.

**Change the DNS backend to `smartdns`:**  
No further configuration is needed, adblock deposits the final blocklist `adb_list.overall` in `/tmp/smartdns` by default.

**Service status output:**  
In LuCI you'll see the realtime status in the `Runtime` section on the overview page.
To get the status in the CLI, just call _/etc/init.d/adblock status_ or _/etc/init.d/adblock status\_service_:

```sh
root@blackhole:~# /etc/init.d/adblock status
::: adblock runtime information
  + adblock_status  : enabled
  + frontend_ver    : 4.5.2-r4
  + backend_ver     : 4.5.2-r4
  + blocked_domains : 888 135
  + active_feeds    : 1hosts, adguard, adguard_tracking, bitcoin, certpl, doh_blocklist, hagezi, ipfire_dbl, phishing_army, smarttv_tracking, stevenblack, winspy
  + dns_backend     : unbound (1.24.2-r1), /mnt/data/adblock/backup, 346.57 MB
  + run_ifaces      : trigger: wan, report: br-lan
  + run_information : base: /mnt/data/adblock, dns: /var/lib/unbound, backup: /mnt/data/adblock/backup, report: /mnt/data/adblock/report, error: /dev/null
  + run_flags       : shift: ✔, custom feed: ✘, ext. DNS (std/prot/remote/bridge): ✘/✔/✔/✔, force: ✔, flush: ✘, tld: ✔, search: ✘, report: ✔, mail: ✔, jail: ✘, debug: ✘
  + last_run        : mode: reload, 2026-03-12T19:08:41+01:00, duration: 0m 57s, 1337.01 MB available
  + system_info     : cores: 4, fetch: curl, Bananapi BPI-R3, mediatek/filogic, OpenWrt SNAPSHOT (r33360-ab0872a734)
```

<a id="best-practice-and-tweaks"></a>
## Best practice and tweaks

**Recommendation for low memory systems**  
adblock keeps all working data in RAM to avoid unnecessary flash wear. The number of parallel processing jobs and the sort buffer size are automatically scaled to the available memory, so on constrained devices adblock already throttles itself during feed processing. On devices with only 128–256 MB RAM, you can further reduce memory pressure with the following optimizations:
* Limit CPU parallelism: the auto-detected core count is capped to the available memory; set `adb_cores=1` to force single-threaded processing with minimal peak memory. A manually set value is always used as-is — it is never lowered, so raising it on a constrained device is at your own risk
* Use external storage: with the defaults the feed backups (`adb_backupdir`) and the final blocklist (`adb_dnsdir`) are kept in RAM permanently, not only during feed processing, e.g. 72 MB backups and a 53 MB blocklist with 1.1M entries. Set `adb_basedir`, `adb_backupdir` and `adb_reportdir` to a USB drive or SSD to offload the temporary and persistent data
* Enable blocklist shifting: activate `adb_dnsshift` to store the generated blocklist in the backup directory on external storage and keep only a symlink in RAM
* Use firewall‑based DNS redirection: Route DNS queries via nftables to external filtered DNS resolvers and keep only a minimal local blocklist active
* Use compressed swap: install the `zram-swap` package so the kernel can page out cold memory into compressed RAM under pressure (see below)

**Top level domain compression (adb_tld)**  
The top level domain compression is enabled by default. It removes all subdomains of already blocked domains (e.g. `ads.example.com` if `example.com` is blocked as well), for this it needs an additional label reversal per feed and an extra pass over the merged blocklist. How much it saves depends on the selected feeds: curated feeds like hagezi or oisd are largely compressed already, feeds with many plain host entries are not. Some measurements on 2-core routers:
* Large feed selection with many host entries (1hosts, adguard, adguard_tracking, certpl, doh_blocklist, hagezi, ipfire_dbl, stevenblack, winspy), unbound, x86: the processing time rises from 78 to 109 seconds, but the blocklist shrinks from 2.94M to 1.07M entries (-64%). unbound loads it in 15 instead of 57 seconds and needs 400 MB instead of 1.1 GB RAM, including the restart of the DNS backend the whole run is even faster with `adb_tld=1`
* Small feed selection (anti_ad, hagezi, yoyo), dnsmasq, ARM: the processing time rises by 2 seconds, the blocklist shrinks from 113k to 82k entries (-27%), dnsmasq needs 2.3 MB less RAM
* Curated feeds only (hagezi pro, oisd big), dnsmasq: the processing time drops from 14.5 to 7.5 seconds with `adb_tld=0`, the blocklist grows by 1.3% (~4,700 entries)

Recommendation: keep the default. Only on slow CPUs with curated, already compressed feeds `adb_tld=0` can save processing time at the cost of a slightly larger blocklist. To check it for your own feed selection, compare the number of blocked domains in the adblock status with and without the compression.

**Use compressed swap (zram-swap) on low memory devices**  
The simplest way to survive the memory peak during feed processing is to give the kernel a compressed swap device and let it page out cold memory under pressure. The `zram-swap` package sets this up automatically at boot — no scripting, no changes to adblock, and adblock benefits transparently. Because the swap lives in compressed RAM rather than on flash, this incurs no flash wear.

```sh
apk add zram-swap
```
Size the swap device relative to your physical RAM — a sensible rule of thumb is half to one times the installed RAM. Note that the compressed pages occupy RAM themselves, so do not oversize it on very small devices:

| Installed RAM | Suggested `zram_size_mb` |
| :------------ | :----------------------- |
| 128 MB        | 64–128                   |
| 256 MB        | 128–256                  |
| 512 MB        | 256–512                  |
| 1 GB and more | 512                      |

The device size is configured in `/etc/config/system` via LuCI (System -> ZRam Settings) or via CLI:

```sh
uci set system.@system[0].zram_size_mb='128'
uci commit system
/etc/init.d/zram restart
```

A running zram device cannot be resized in place, so the `restart` is required for a changed size to take effect.  
To make the kernel reclaim into zram more eagerly during the short processing peak, raise the swappiness and persist it across reboots, e.g.:

```sh
echo 'vm.swappiness=100' >> /etc/sysctl.conf
sysctl -p
```

**Please note:** zram covers the short memory peak during feed processing, it does not replace external storage for the backups and the shifted blocklist (see above): without it, they occupy RAM permanently, and the gzip compressed backups can't be compressed any further by zram. On very small devices (128 MB) compressed swap helps, but heavy swapping costs CPU — if RAM is truly marginal, the more honest fix is to activate fewer feeds.

**Sensible choice of blocklists**  
Don`t just blindly activate too many feeds at once, sooner or later this will lead to OOM conditions.The following feeds are just my personal recommendation as an initial setup:
* `adguard`, `adguard_tracking` and `certpl`

In total, this feed selection blocks about 280K domains. It may also be useful to include compilations like hagezi, stevenblack or oisd.

**DNS reporting, enable the GeoIP Map**  
adblock includes a powerful reporting tool on the DNS Report tab which shows the latest DNS statistics generated by tcpdump. To get the latest statistics always press the "Refresh" button.

In addition to a tabular overview adblock reporting includes a GeoIP map in a modal popup window/iframe that shows the geolocation of your own uplink addresses (in green) and the locations of blocked domains in red. To enable the GeoIP Map set the following option in "Advanced Report Settings" config tab: set `adb_map` to `1` to include the external components listed below and activate the GeoIP map.

To make this work, adblock uses the following external components:
* [Leaflet](https://leafletjs.com/) is a lightweight open-source JavaScript library for interactive maps
* The free and quite fast [IP Geolocation API](https://ip-api.com/) to resolve the required IP/geolocation information (max. 45 blocked Domains per request)

The basemap is no longer pulled from a tile service. CARTO started to require an API key for the raster basemaps at basemaps.cartocdn.com and watermarks every unauthenticated tile request, and a key is bound to a single customer, so it cannot be shipped with a package that lands on every installation. adblock therefore draws the basemap from country outlines that come with `luci-app-adblock`: [Natural Earth](https://www.naturalearthdata.com) 1:110m, public domain and stripped of all attributes. The map page issues no request to a third party, works without a WAN connection and leaks no part of the admin session to a CDN. The outlines are enough to locate an IP, so the map does not zoom in beyond level 6 and labels the continents rather than the countries.

**Optional: a higher detail basemap**

The shipped 1:110m outlines are coarse around Scandinavia, the Greek islands and the smaller island states. If you want sharper coastlines, build the 1:50m variant with [mapshaper](https://github.com/mbloch/mapshaper) and drop it next to the shipped file. LuCI looks for it on every map run and falls back to the shipped outlines when it is missing, no config option is involved:

```
curl -sSLo ne50.geojson https://raw.githubusercontent.com/nvkelso/natural-earth-vector/v5.1.2/geojson/ne_50m_admin_0_countries.geojson
mapshaper ne50.geojson -filter-fields -simplify 5% keep-shapes -o force precision=0.01 format=geojson world-50m.json
scp world-50m.json root@openwrt:/www/luci-static/resources/view/adblock/
```

The result is roughly 105 kB, about three times the shipped file. This file is not part of any package, so it is removed on sysupgrade unless you add its path to `/etc/sysupgrade.conf`, and it stays behind when `luci-app-adblock` is uninstalled.

**DNS reporting, limit the tcpdump capture**  
`adb_repfilter` takes a regular tcpdump/BPF expression which is logically ANDed to the internal reporting port filter. It narrows the capture for every `adb_repiface` setting, not just for `any`, e.g. to skip a single noisy client or to limit the report to certain network segments.

The most common use case is a capture with `adb_repiface` set to `any`, where tcpdump also picks up DNS traffic on outbound interfaces, e.g. requests that originate from the router itself and are routed through a VPN client interface. Those packets are captured correctly, but they are redundant, they clutter the report and they consume the limited pcap ring buffer configured via `adb_repchunkcnt` and `adb_repchunksize`. Some examples:

| Expression                                         | Purpose                                                             |
| :------------------------------------------------- | :------------------------------------------------------------------ |
| `not net 10.0.0.0/24`                              | skip a VPN transfer network                                         |
| `not (net 10.0.0.0/24 or net fd00:dead:beef::/64)` | skip a VPN transfer network, IPv4 and IPv6                          |
| `net 192.168.1.0/24 or net 192.168.2.0/24`         | limit the capture to selected LAN segments                          |
| `not host 192.168.1.10`                            | skip a single noisy client                                          |
| `not (host 192.168.1.10 or host 192.168.1.11)`     | skip several clients                                                |
| `not host 9.9.9.9`                                 | skip the forwarding between the router and its upstream resolver    |
| `ip`                                               | capture IPv4 only                                                   |

Please note:
* the expression must not drop one direction of a DNS transaction. A report line is only emitted once the answer to a pending query has been seen, so filters like `inbound`, `src net ...` or `dst port 53` compile fine but result in an empty report
* `net 10.0.0.1` is not a subnet, without a prefix length libpcap assumes /32 and the expression becomes equivalent to `host 10.0.0.1`
* `not host A or not host B` does not skip both hosts, it only skips the traffic between A and B. Use `not (host A or host B)` instead
* address, network and port primitives work with every interface, but the `vlan` and `ether` primitives require an ethernet based capture. They are rejected with `adb_repiface` set to `any` (linux cooked mode) as well as on tunnel or ppp interfaces like `wg0` or `pppoe-wan`
* the expression can only narrow the capture, additional ports have to be added via `adb_repport`
* a too narrow expression still compiles and tcpdump starts normally, it just yields an empty report. Keep that in mind when `adb_repiface` is changed while a filter is set, the active filter is part of the runtime status

**External adblock test**  
In addition to the built‑in DNS reporting and GeoIP map, adblock users can verify the effectiveness of their configuration with an external test page. The [Adblock Test](https://adblock.turtlecute.org/) provides a simple way to check whether your current adblock setup is working as expected. It loads a series of test elements (ads, trackers, and other resources) and reports whether they are successfully blocked by your configuration.

The test runs entirely in the browser and does not require additional configuration. For best results, open the page in the same environment where adblock is active and review the results displayed.

**Firewall‑Based DNS Control**  
adblock provides several advanced firewall‑integrated features that allow you to enforce DNS policies directly at the network layer. These mechanisms operate independently of the local DNS resolver and ensure that DNS traffic follows your filtering rules, even when clients attempt to bypass them.
* Unfiltered external DNS Routing: routes DNS queries from selected devices or interfaces to an external unfiltered DNS resolver
* Filtered external DNS Routing: routes DNS queries from selected devices or interfaces to an external filtered DNS resolver
* Force DNS: blocks or redirects all external DNS traffic to ensure that clients use the local resolver

The DNS routing allows you to apply external DNS (unfiltered and/or filtered) to specific devices or entire network segments. DNS queries from these targets are transparently redirected to a chosen external resolver (IPv4 and/or IPv6):
* MAC‑based targeting for individual devices
* Interface/VLAN targeting for entire segments
* Separate IPv4/IPv6 resolver selection
* Transparent DNS redirection without client‑side configuration
This mode is ideal for guest networks, IoT devices, or environments where certain clients require stricter/lesser DNS filtering.

force DNS ensures that all DNS traffic on your network by specific devices or entire network segments is processed by the local resolver. Any attempt to use external DNS servers is blocked or redirected.
* Blocks external DNS on port 53 and redirects DNS queries to the local resolver when appropriate
* Also prevents DNS bypassing by clients with hardcoded DNS settings on other ports, e.g. on port 853
This mode guarantees that adblock’s filtering pipeline is always applied.

adblock's firewall rules are based on nftables in a separate isolated nftables table (inet adblock) and chains (prerouting), with MAC addresses stored in a nftables set. The configuration is carried out centrally in LuCI on the ‘Firewall Settings’ tab in adblock.

**Remote DNS Allow (Temporary MAC‑Based Bypass)**  
This additional firewall feature lets selected client devices temporarily bypass local DNS blocking and use an external, unfiltered DNS resolver. It is designed for situations where a device needs short‑term access to content normally blocked by the adblock rules.

A lightweight CGI endpoint handles the workflow:
* The client opens the URL, e.g. http(s)://\<ROUTER-IP\>/cgi-bin/adblock (preferably transferred via QR code shown in LuCI)
* The script automatically detects the device’s MAC address
* If the MAC is authorized, the script displays the current status:
  * Not in the nftables set → option to request a temporary allow (“Bypass”)
  * Already active → shows remaining timeout
* When renewing, the CGI adds the MAC to an nftables Set with a per‑entry timeout

The CGI interface is mobile‑friendly and includes a LuCI‑style loading spinner during the renew process, giving immediate visual feedback while the nftables entry is created. All operations are atomic and safe even when multiple devices renew access in parallel.

**Temporary DNS Bridging (Zero‑Downtime during DNS Restarts)**  
Adblock can optionally enable a temporary DNS bridging mode to avoid DNS downtime during DNS backend restarts.
When this feature is enabled, all DNS queries from LAN clients are briefly redirected to an external fallback resolver until the local DNS backend becomes available again. This ensures that DNS resolution continues to work seamlessly for all clients, even while adblock reloads blocklists or restarts the DNS service. Just set the options `adb_nftbridge`, `adb_bridgednsv4` and `adb_bridgednsv6` accordingly.

**Jail mode (allowlist-only):**  
Enforces a strict allowlist‑only DNS policy in which only domains listed in the allowlist file are resolved, while every other query is rejected. This mode is intended for highly restrictive environments and depends on a carefully maintained allowlist, typically managed manually.

**Download options**  
By default adblock uses the following pre-configured download options:

```
    * curl: --connect-timeout 20 --retry-delay 10 --retry 4 --retry-max-time 80 --retry-all-errors --fail --silent --show-error --location -o
    * wget: --no-cache --no-cookies --timeout=20 --waitretry=10 --tries=5 --retry-connrefused -O
    * uclient-fetch: --timeout=20 -O
```

To override the default set `adb_fetchretry`, `adb_fetchinsecure` or globally `adb_fetchparm` to your needs.

**Enable E-Mail notification via `msmtp`:**  
To use the email notification you have to install & configure the package `msmtp`.
Modify the file `/etc/msmtprc`:
<pre><code>
[...]
defaults
auth            on
tls             on
tls_certcheck   off
timeout         5
syslog          LOG_MAIL
[...]
account         adb_notify
host            smtp.gmail.com
port            587
from            dev.adblock@gmail.com
user            dev.adblock
password        xxx
</code></pre>
Finally enable E-Mail support, add a valid E-Mail receiver address in LuCI and setup an appropriate cron job.

**Automatic adblock feed updates and E-Mail reports**  
For a regular, automatic update of the used feeds or other regular adblock tasks set up a cron job. Use `reload` here — `start`/`restart` would only restore the backups instead of fetching fresh feeds. In LuCI you find the cron settings under `System` => `Scheduled Tasks`. On the command line the cron file is located at `/etc/crontabs/root`:  

Example 1
```sh
# update the adblock feeds every morning at 4 o'clock
00 04 * * * /etc/init.d/adblock reload
```

Example 2
```sh
# update the adblock feeds every hour
0 */1 * * * /etc/init.d/adblock reload
```

Example 3
```sh
# send an adblock E-Mail report every morning at 3 o'clock
00 03 * * * /etc/init.d/adblock report mail
```

**Change/add adblock feeds**  
The adblock default blocklist feeds are stored in an external JSON file `/etc/adblock/adblock.feeds`. This file is shipped with the package and is **overwritten on every package update**, so never edit it directly. All of your custom changes belong in the separate JSON file `/etc/adblock/adblock.custom.feeds` (empty by default), which is preserved across updates. It's recommended to use the LuCI based Custom Feed Editor (`Custom Feed Editor` tab), which validates the JSON for you.

**Please note:** if `/etc/adblock/adblock.custom.feeds` exists and is non-empty, it is loaded **instead of** the shipped `adblock.feeds` — it replaces the feed set, it does not merge with it. A custom feed file must therefore contain *every* feed you want active, not just your additions. The Custom Feed Editor handles this for you by working on a full copy.

A feed is a single JSON object, keyed by a unique feed name (no spaces, no special characters). Example:

```json
	[...]
	"stevenblack": {
		"url": "https://raw.githubusercontent.com/StevenBlack/hosts/master/",
		"rule": "feed 0.0.0.0 2",
		"size": "VAR",
		"descr": "compilation"
	},
	[...]
```

The object supports the following fields:

| Field | Required | Description                                                                                               |
| :---- | :------: | :-------------------------------------------------------------------------------------------------------- |
| url   | yes      | download URL of the domain list (for category feeds: the base URL, see below)                             |
| rule  | yes      | the parsing ruleset, max. 4 space separated parameters (see below)                                        |
| size  | yes      | size hint shown in LuCI: `S`, `M`, `L`, `XL`, `XXL` or `VAR` (see the feed table legend in Main Features) |
| descr | yes      | a short human-readable description shown in LuCI and the feed table                                       |

**The `rule` field**  
The rule consists of max. 4 individual, space separated parameters:
1. **type**: always `feed` (required). adblock only supports the `feed` type for external sources; any source whose rule does not start with `feed` is skipped.
2. **prefix**: an optional search term (a literal string, not a regex) that a line must contain to be treated as a valid entry. Use it to pick the relevant rows of a hosts-style file, e.g. `0.0.0.0`. Omit it for a plain list with one bare domain per line.
3. **column**: the 1-based column that holds the domain within a matching line, e.g. `2` for a `0.0.0.0 example.com` hosts file or `1` for a bare list (required). When no prefix is used, give the column directly, e.g. `feed 1`.
4. **separator**: an optional field separator; default is the whitespace character class `[[:space:]]+`. Pass a literal character such as `,` for comma-separated sources.

Examples:
* `feed 1` — plain list, one domain per line, domain in column 1
* `feed 0.0.0.0 2` — hosts-style file, keep only `0.0.0.0` lines, domain in column 2
* `feed 1 ,` — comma-separated source, domain in the first field

**Category-based feeds (size `VAR`)**  
The feeds marked `VAR` in the feed table (`1hosts`, `hagezi`, `ipfire_dbl`, `stevenblack`, `utcapitole`) are built-in feeds that require an additional category selection via the dedicated options `adb_hst_feed`, `adb_hag_feed`, `adb_ipf_feed`, `adb_stb_feed` and `adb_utc_feed` (or the LuCI feed configuration). For these, the `url` is a **base URL** to which the selected category is appended at download time. This category mechanism is wired to those specific feed names in the backend, so it cannot be reused for arbitrary new feeds — a custom feed you add yourself should point `url` at a single, complete list URL.

After editing `/etc/adblock/adblock.custom.feeds`, reload adblock (`/etc/init.d/adblock reload`) and check the `Log View` tab (or `logread -e adblock-`). With `adb_debug` enabled, a malformed JSON object or a wrong column/separator typically shows up there as a feed that produces zero domains.

<a id="troubleshooting-and-debug-options"></a>
## Troubleshooting & debug options
Adblock provides an optional debug mode that writes diagnostic information to the system log and captures internal error output in a dedicated error logfile - by default located in the adblock base directory as `/tmp/adb_error.log`. The log file is automatically cleared at the beginning of each run. Under normal conditions, all error messages are discarded to keep regular runs clean and silent. To enable debug mode, set the option `adb_debug` to `1`. When enabled, the script produces significantly more log output to assist with troubleshooting.

Whenever you encounter adblock related processing problems, please enable debug logging, restart adblock and check the `Log View` tab in LuCI (or the syslog via `logread -e adblock-`).

<a id="support"></a>
## Support
Please join the adblock discussion in this [forum thread](https://forum.openwrt.org/t/adblock-support-thread/507) or contact me by mail <dev@brenken.org>

<a id="removal"></a>
## Removal
Stop all adblock related services with _/etc/init.d/adblock stop_ and remove the adblock package if necessary, e.g.:

```sh
/etc/init.d/adblock stop
apk del luci-app-adblock adblock
rm -f /etc/config/adblock
rm -rf /tmp/luci-*
```

<a id="donations"></a>
## Donations
You like this project - is there a way to donate? Generally speaking "No" - I have a well-paying full-time job and my OpenWrt projects are just a hobby of mine in my spare time.

If you still insist to donate some bucks ...
* I would be happy if you put your money in kind into other, social projects in your area, e.g. a children's hospice
* Let's meet and invite me for a coffee if you are in my area, the “Markgräfler Land” in southern Germany or in Switzerland (Basel)
* Send your money to my [PayPal account](https://www.paypal.me/DirkBrenken) and I will collect your donations over the year to support various social projects in my area

No matter what you decide - thank you very much for your support!

Have fun!  
Dirk

---

### Overview (`net/apfree-wifidog/README.md`)

[![license][1]][2]
[![PRs Welcome][3]][4]
[![Issues Welcome][5]][6]
[![Release Version][7]][8]
[![OpenWrt][11]][12]
[![Join the QQ Group][15]][16]


[1]: https://img.shields.io/badge/license-GPLV3-brightgreen.svg?style=plastic
[2]: https://github.com/liudf0716/apfree_wifidog/blob/master/COPYING
[3]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=plastic
[4]: https://github.com/liudf0716/apfree_wifidog/pulls
[5]: https://img.shields.io/badge/Issues-welcome-brightgreen.svg?style=plastic
[6]: https://github.com/liudf0716/apfree_wifidog/issues/new
[7]: https://img.shields.io/badge/release-3.11.1716-red.svg?style=plastic
[8]: https://github.com/liudf0716/apfree_wifidog/releases
[11]: https://img.shields.io/badge/Platform-%20OpenWrt%7C%20LEDE%20-brightgreen.svg?style=plastic
[12]: https://github.com/KunTengRom/kunteng-lede-17.01.4
[13]: https://img.shields.io/badge/KunTeng-Inside-blue.svg?style=plastic
[14]: https://www.kunteng.org.cn
[15]: https://img.shields.io/badge/chat-qq%20group-brightgreen.svg
[16]: https://jq.qq.com/?_wv=1027&k=4ADDSev

## ApFree WiFiDog: A high performance captive portal solution for HTTP(s)

ApFree WiFiDog is a high performance captive portal solution for HTTP(s), which mainly used in ([LEDE](https://github.com/lede-project/source) & [OpenWrt](https://github.com/openwrt/openwrt)) platform. 


**[中文介绍](https://github.com/liudf0716/apfree_wifidog/blob/master/README_ZH.md)**

## Enhancement of apfree-wifidog 

In fact, the title should be why we choose apfree-wifidog, the reason was the following: 

>  Stable

apfree-wifidog was widely used in tens of thousands device, which were running in business scene. In order to improve its stable, we rewrite all iptables rule by api instead of fork call, which will easily cause deadlock in multithread-fork running environment. we also re-write the code and replace libhttpd (which is unmaintained for years) with libevent

> Performance

apfree-wifidog's http request-response is more quick, u can find statistic data in our test document

> HTTPs redirect

apfree-wifidog support https redirect, in current internet environment, captive portal solution without supporting https redirect will become unsuitable gradually


> More features

apfree-wifidog support mac temporary-pass, ip,domain,pan-domain,white-mac,black-mac rule and etc. all these rules can be applied without restarting wifidog

> MQTT support

by enable mqtt support, u can remotely deliver such as trusted ip, domain and pan-domain rules to apfree wifidog 

> Compilable with wifidog protocol

u don't need to modify your wifidog authentication server to adapt apfree-wifidog; if u have pression on server-side, apfree wifidog's improved protocol can greatly relieve it, which disabled by default


## Getting started

before starting apfree-wifidog, we must know how to configure it. apfree-wifidog use OpenWrt standard uci config system, all your apfree-wifidog configure information stored in `/etc/confg/wifidogx`, which will be parsed by  `/etc/init.d/wifidogx` to /tmp/wifidog.conf, apfree-wifidog's real configure file is `/tmp/wifidog.conf`

The default apfree-wifidog UCI configuration file like this:

```
config wifidog
    option  gateway_interface   'br-lan'
    option  auth_server_hostname    'wifidog.kunteng.org.cn'
    option  auth_server_port    443
    option  auth_server_path    '/wifidog/'
    option  check_interval      60
    option  client_timeout      5
    option  apple_cna           1
    option  thread_number       5
    option  wired_passed        0
    option  enable      0
```

> auth_server_hostname was apfree-wifidog auth server, it can be domain or ip; wifidog.kunteng.org.cn is a free auth server we provided, it was also [open source](https://github.com/wificoin-project/wwas) 

> apple_cna 1 apple captive detect deceive; 2 apple captive detect deceive to  disallow portal page appear

> wired_passed means whether LAN access devices need to auth or not, value 1 means no need to auth 

> enable means whether start apfree-wifidog when we executed `/etc/init.d/wifidogx start`, if u wanted to start apfree-wifidog, you must set enable to 1 before executing `/etc/init.d/wifidogx start`

### How to support https redirect

In order to support https redirect, apfree-wifidog need x509 pem cert and private key, u can generate yourself like this:

```
PX5G_BIN="/usr/sbin/px5g"
OPENSSL_BIN="/usr/bin/openssl"
APFREE_CERT="/etc/apfree.crt"
APFREE_KEY="/etc/apfree.key"

generate_keys() {
    local days bits country state location commonname

    # Prefer px5g for certificate generation (existence evaluated last)
    local GENKEY_CMD=""
    local UNIQUEID=$(dd if=/dev/urandom bs=1 count=4 | hexdump -e '1/1 "%02x"')
    [ -x "$OPENSSL_BIN" ] && GENKEY_CMD="$OPENSSL_BIN req -x509 -sha256 -outform pem -nodes"
    [ -x "$PX5G_BIN" ] && GENKEY_CMD="$PX5G_BIN selfsigned -pem"
    [ -n "$GENKEY_CMD" ] && {
        $GENKEY_CMD \
            -days ${days:-730} -newkey rsa:${bits:-2048} -keyout "${APFREE_KEY}.new" -out "${APFREE_CERT}.new" \
            -subj /C="${country:-CN}"/ST="${state:-localhost}"/L="${location:-Unknown}"/O="${commonname:-ApFreeWiFidog}$UNIQUEID"/CN="${commonname:-ApFreeWiFidog}"
        sync
        mv "${APFREE_KEY}.new" "${APFREE_KEY}"
        mv "${APFREE_CERT}.new" "${APFREE_CERT}"
    }
}

```

or when u start `/etc/init.d/wifidogx start`, it will generate it automatically


For more information, please refer to the upstream [project page](https://github.com/liudf0716/apfree_wifidog)

---

### Overview (`net/banip/README.md`)

# banIP - ban incoming and outgoing IP addresses/subnets via Sets in nftables

## Table of Contents
* [Description](#description)
* [Quick Start](#quick-start)
* [Main Features](#main-features)
* [Prerequisites](#prerequisites)
* [Installation and Usage](#installation-and-usage)
* [banIP CLI interface](#banip-cli-interface)
* [banIP config options](#banip-config-options)
* [Examples](#examples)
* [Best practise and tweaks](#best-practise-and-tweaks)
* [Troubleshooting & debug options](#troubleshooting-and-debug-options)
* [Support](#support)
* [Removal](#removal)
* [Donations](#donations)

<a id="description"></a>
## Description
IP address blocking is commonly used to protect against brute force attacks, prevent disruptive or unauthorized address(es) from access or it can be used to restrict access to or from a particular geographic area — for example. Further more banIP scans the log file via logread and bans IPs that make too many password failures, e.g. via ssh.

<a id="quick-start"></a>
## Quick Start
For a typical setup these few steps are enough to get banIP up and running — see the sections below for details:
1. Install the LuCI companion package: `apk update && apk add luci-app-banip` (this pulls in the `banip` backend as a dependency).
2. Open LuCI under `Services → banIP`, tick `Enabled` and (recommended) set a `Startup Trigger Interface` to your WAN interface(s).
3. Activate a small, sensible feed selection to start with, e.g. `cinsscore`, `debl`, `turris` and `doh` in their default chains (≈20K IPs).
4. Start and verify the service:

```sh
/etc/init.d/banip start
/etc/init.d/banip status
```

**Please note:** don't blindly enable (too) many feeds at once — on low memory devices this will sooner or later lead to OOM conditions.

<a id="main-features"></a>
## Main Features
* banIP supports the following fully pre-configured IP blocklist feeds (free for private usage, for commercial use please check their individual licenses).
**Please note:** By default, each feed blocks the packet flow in the chain(s) shown in the table below. _Inbound_ combines the chains WAN-Input and WAN-Forward, _Outbound_ represents the LAN-FWD chain:
  * WAN-INP chain applies to packets from internet to your router
  * WAN-FWD chain applies to packets from internet to other local devices (not your router)
  * LAN-FWD chain applies to local packets going out to the internet (not your router)

  How to read the default direction: most reputation feeds list the **source** IPs of attackers, scanners and spammers — those are blocked _inbound_, because the unwanted connection is initiated **from the internet towards you**. A second group of feeds lists IPs that your **own clients should never talk to** (malware hosting, command-and-control servers, threat/DoH/DNS endpoints); those are blocked _outbound_ in the LAN-FWD chain, because the connection is initiated **from your LAN**. Feeds where both risks apply (e.g. spamhaus, emerging threats, tor, proxy, vpn) default to **both** directions. This distinction matters: a feed of malware-download or C2 IPs in the _inbound_ chain alone provides little protection, because your LAN client is the one initiating the connection — see the `country`/`asn` note in the [Best practise](#best-practise-and-tweaks) section for the common "block a country" case.

  The listed standard assignments can be changed to your needs under the `Feed/Set Settings` config tab (options `ban_feedin`, `ban_feedout`, `ban_feedinout` and `ban_feedreset`).

| Feed                | Focus                          | Inbound | Outbound | Proto/Port        | Information                                                  |
| :------------------ | :----------------------------- | :-----: | :------: | :---------------: | :----------------------------------------------------------- |
| asn                 | ASN segments                   |    x    |          |                   | [Link](https://asn.ipinfo.app)                               |
| backscatterer       | backscatterer IPs              |    x    |          |                   | [Link](https://www.uceprotect.net/en/index.php)              |
| becyber             | malicious attacker IPs         |    x    |          |                   | [Link](https://github.com/duggytuxy/malicious_ip_addresses)  |
| binarydefense       | binary defense banlist         |    x    |          |                   | [Link](https://iplists.firehol.org/?ipset=bds_atif)          |
| bogon               | bogon prefixes                 |    x    |          |                   | [Link](https://team-cymru.com)                               |
| bruteforceblock     | bruteforceblocker IPs          |    x    |          |                   | [Link](https://danger.rulez.sk/index.php/bruteforceblocker/) |
| country             | country blocks                 |    x    |          |                   | [Link](https://www.ipdeny.com/ipblocks)                      |
| cinsscore           | suspicious attacker IPs        |    x    |          |                   | [Link](https://cinsscore.com/#list)                          |
| debl                | fail2ban IP blacklist          |    x    |          |                   | [Link](https://www.blocklist.de)                             |
| dns                 | public DNS-Server              |         |    x     | tcp, udp: 53, 853 | [Link](https://public-dns.info)                              |
| doh                 | public DoH-Server              |         |    x     | tcp, udp: 80, 443 | [Link](https://github.com/dibdot/DoH-IP-blocklists)          |
| dshield             | dshield IP blocklist           |    x    |          |                   | [Link](https://www.dshield.org)                              |
| etcompromised       | ET compromised hosts           |    x    |          |                   | [Link](https://iplists.firehol.org/?ipset=et_compromised)    |
| feodo               | feodo tracker                  |    x    |    x     |                   | [Link](https://feodotracker.abuse.ch)                        |
| firehol1            | firehol level 1 compilation    |    x    |          |                   | [Link](https://iplists.firehol.org/?ipset=firehol_level1)    |
| firehol2            | firehol level 2 compilation    |    x    |          |                   | [Link](https://iplists.firehol.org/?ipset=firehol_level2)    |
| firehol3            | firehol level 3 compilation    |    x    |          |                   | [Link](https://iplists.firehol.org/?ipset=firehol_level3)    |
| firehol4            | firehol level 4 compilation    |    x    |          |                   | [Link](https://iplists.firehol.org/?ipset=firehol_level4)    |
| greensnow           | suspicious server IPs          |    x    |          |                   | [Link](https://greensnow.co)                                 |
| hagezi              | Threat IP blocklist            |         |    x     | tcp, udp: 80, 443 | [Link](https://github.com/hagezi/dns-blocklists)             |
| ipblackhole         | blackhole IPs                  |    x    |          |                   | [Link](https://github.com/BlackHoleMonster/IP-BlackHole)     |
| ipexdbl             | IPEX dynamic blocklists        |    x    |          |                   | [Link](https://github.com/ZEROF/ipextractor)                 |
| ipsum               | malicious IPs                  |    x    |          |                   | [Link](https://github.com/stamparm/ipsum)                    |
| ipthreat            | hacker and botnet IPs          |    x    |          |                   | [Link](https://ipthreat.net)                                 |
| myip                | real-time IP blocklist         |    x    |          |                   | [Link](https://myip.ms)                                      |
| proxy               | open proxies                   |    x    |    x     |                   | [Link](https://iplists.firehol.org/?ipset=proxylists)        |
| spamhaus            | Spamhaus DROP                  |    x    |    x     |                   | [Link](https://www.spamhaus.org/blocklists/)                 |
| threat              | emerging threats               |    x    |    x     |                   | [Link](https://rules.emergingthreats.net)                    |
| threatview          | malicious IPs                  |    x    |    x     |                   | [Link](https://threatview.io)                                |
| tor                 | tor exit nodes                 |    x    |    x     |                   | [Link](https://www.dan.me.uk)                                |
| turris              | turris sentinel blocklist      |    x    |          |                   | [Link](https://view.sentinel.turris.cz)                      |
| uceprotect1         | spam protection level 1        |    x    |          |                   | [Link](https://www.uceprotect.net/en/index.php)              |
| uceprotect2         | spam protection level 2        |    x    |          |                   | [Link](https://www.uceprotect.net/en/index.php)              |
| uceprotect3         | spam protection level 3        |    x    |          |                   | [Link](https://www.uceprotect.net/en/index.php)              |
| urlhaus             | urlhaus IDS IPs                |         |    x     | tcp, udp: 80, 443 | [Link](https://urlhaus.abuse.ch)                             |
| urlvir              | malware related IPs            |         |    x     | tcp, udp: 80, 443 | [Link](https://iplists.firehol.org/?ipset=urlvir)            |
| webclient           | malware related IPs            |         |    x     | tcp, udp: 80, 443 | [Link](https://iplists.firehol.org/?ipset=firehol_webclient) |
| voip                | VoIP fraud blocklist           |    x    |          |                   | [Link](https://voipbl.org)                                   |
| vpn                 | vpn IPs                        |    x    |    x     |                   | [Link](https://github.com/X4BNet/lists_vpn)                  |
| vpndc               | vpn datacenter IPs             |    x    |    x     |                   | [Link](https://github.com/X4BNet/lists_vpn)                  |

* Zero-conf like automatic installation & setup, usually no manual changes needed
* All Sets are handled in a separate nft table/namespace `banIP`
* Full IPv4 and IPv6 support
* Supports nft atomic Set loading
* Supports blocking by ASN numbers and by iso country codes
* Block countries dynamically by Regional Internet Registry (RIR), e.g. all countries related to ARIN. Supported service regions are: AFRINIC, ARIN, APNIC, LACNIC and RIPE
* Supports local allow- and blocklist with MAC/IPv4/IPv6 addresses or domain names
* Supports concatenation of local MAC addresses with IPv4/IPv6 addresses, e.g. to enforce dhcp assignments
* All local input types support ranges in CIDR notation
* Auto-add the uplink subnet or uplink IP to the local allowlist
* Prevent common ICMP, UDP and SYN flood attacks and drop spoofed tcp flags & invalid conntrack packets (DoS attacks) in an additional prerouting chain
* Provides a background log monitor to ban unsuccessful login attempts in real-time (like fail2ban, crowdsec etc.) with three-tier IP deduplication, dynamic cache management and optional RDAP-based subnet blocking
* Auto-add unsuccessful LuCI, Asterisk or ssh login attempts to the local blocklist
* Auto-add entire subnets to the blocklist Set based on an additional RDAP request with the monitored suspicious IP
* Fast feed processing as they are handled in parallel as background jobs (on capable multi-core hardware)
* Per feed it can be defined whether the inbound chain (wan-input, wan-forward) or the outbound chain (lan-forward) should be blocked
* Automatic blocklist backup & restore, the backups will be used in case of download errors or during startup
* Automatically selects one of the following download utilities with ssl support: curl, uclient-fetch or full wget
* Provides HTTP ETag support to download only resources that have been updated on the server side, to speed up banIP reloads and to save bandwidth
* Supports an `allowlist only` mode, this option restricts the internet access only to specific, explicitly allowed IP segments
* Optionally screens the allowed IP segments in `allowlist only` mode as well, i.e. the log monitor may ban attackers from an explicitly allowed segment while the local allowlist entries stay exempt (see `ban_monitorallowed`)
* Supports external allowlist URLs to reference additional IPv4/IPv6 feeds
* Optionally always allow certain protocols/destination ports in the inbound chain
* Deduplicate IPs across all Sets (single IPs only, no intervals)
* Implements BCP38 ingress filtering to prevent IP address spoofing
* Provides comprehensive runtime information
* Provides a detailed Set report, incl. a map that shows the geolocation of your own uplink addresses (in green) and the location of potential attackers (in red), drawn from local country outlines without any tile service
* Provides a Set search engine for certain IPs
* Feed parsing by fast & flexible regex rulesets
* Minimal status & error logging to syslog, enable debug logging to receive more output
* Procd based init system support (start/stop/restart/reload/status/report/search/content)
* Procd network interface trigger support
* Add new or edit existing banIP feeds on your own with the LuCI integrated custom feed editor
* Supports destination port & protocol limitations for external feeds (see the feed list above). To change the default assignments just use the custom feed editor
* Supports allowing / blocking of certain VLAN forwards
* Provides an option to transfer logging events on remote servers via cgi interface

<a id="prerequisites"></a>
## Prerequisites
* **[OpenWrt](https://openwrt.org)**, latest stable release or a development snapshot with nft/firewall 4 support
* A download utility with SSL support: `curl`, full `wget` or `uclient-fetch` with one of the `libustream-*` SSL libraries, the latter one doesn't provide support for ETag HTTP header
* A certificate store like `ca-bundle`, as banIP checks the validity of the SSL certificates of all download sites by default
* For E-Mail notifications you need to install and setup the additional `msmtp` package

**Please note:**  
* Devices with less than 256MB of RAM are **_not_** supported
* After system upgrades it's recommended to start with a fresh banIP default config
* Only `reload` actually refreshes the feeds (ETag check plus download of changed feeds). `start`, `restart` — and `boot`/`resume` — restore the existing blocklist backups and only download feeds that have **no** backup yet; they do **not** re-fetch already cached feeds. To update your blocklists (e.g. from a cron job) always use `reload`.  


<a id="installation-and-usage"></a>
## Installation and Usage
* Update your router's apk repository (apk update)
* Install the LuCI companion package `luci-app-banip` which also installs the main banIP package as a dependency
* Enable the banIP system service (System -> Startup) and enable banIP itself (banIP -> General Settings)
* It's strongly recommended to use the LuCI frontend to easily configure all aspects of banIP, the application is located in LuCI under the `Services` menu
* It's also recommended to configure a `Startup Trigger Interface` to depend on your WAN interface events during boot or restart of your router. Listing IPv6 interfaces (wan6) is fine as well: an interface event that only changed the uplink addresses refreshes the auto-allowed uplink entries in place, without a full banIP restart. This keeps the allowlist in sync with a dynamic IPv6 prefix
* To be able to use banIP in a meaningful way, you must activate the service and possibly also activate a few blocklist feeds
* If you're using a complex network setup, e.g. special tunnel interfaces, then untick the `Auto Detection` option under the `General Settings` tab and set the required options manually
* Start the service with `/etc/init.d/banip start` and check everything is working by running `/etc/init.d/banip status`, also check the `Processing Log` tab

<a id="banip-cli-interface"></a>
## banIP CLI interface
* All important banIP functions are accessible via CLI, too. If you're going to configure banIP via CLI, edit the config file `/etc/config/banip` and enable the service, add pre-configured feeds and add/change other options to your needs, see the options reference table below.

```sh
~# /etc/init.d/banip
Syntax: /etc/init.d/banip [command]

Available commands:
	start           Start the service
	stop            Stop the service
	restart         Restart the service
	reload          Reload configuration files (or restart if service does not implement reload)
	enable          Enable service autostart
	disable         Disable service autostart
	enabled         Check if service is started on boot
	report          [text|json|mail|gen] Print banIP related Set statistics
	search          [<IPv4 address>|<IPv6 address>] Check if an element exists in a banIP Set
	content         [<Set name>] [true|false] Listing of all or only elements with hits of a given banIP Set
	running         Check if service is running
	status          Service status
	trace           Start with syscall trace
	info            Dump procd service info
```

The `report` sub-command accepts an output mode: `text` (default, human-readable table), `json` (machine-readable output, incl. GeoIP map data when `ban_map=1`), `mail` (send the report via `msmtp`) and `gen` (regenerate the report data files in the background, used by the LuCI frontend).

<a id="banip-config-options"></a>
## banIP config options

| Option                  | Type   | Default                       | Description                                                                                                       |
| :---------------------- | :----- | :---------------------------- | :---------------------------------------------------------------------------------------------------------------- |
| ban_enabled             | option | 0                             | enable the banIP service                                                                                          |
| ban_nicelimit           | option | 0                             | ulimit nice level of the banIP service (range 0-19)                                                               |
| ban_filelimit           | option | 1024                          | ulimit max open/number of files (range 1024-4096)                                                                 |
| ban_loglimit            | option | 100                           | scan only the last n log entries permanently. A value of `0` disables the monitor                                 |
| ban_logcount            | option | 1                             | how many times the IP must appear in the log per blocking cycle to trigger auto-blocking                          |
| ban_logterm             | list   | regex                         | various regex for logfile parsing (default: dropbear, sshd, luci, asterisk and cgi-remote events)                 |
| ban_logreadfile         | option | - / logread                   | parse this log file via tail instead of the default logread; by default banIP reads the system log via logread    |
| ban_autodetect          | option | 1                             | auto-detect wan interfaces, devices and subnets                                                                   |
| ban_debug               | option | 0                             | enable banIP related debug logging                                                                                |
| ban_icmplimit           | option | 25                            | threshold in number of packets to detect icmp DoS in prerouting chain. A value of `0` disables this safeguard     |
| ban_synlimit            | option | 10                            | threshold in number of packets to detect syn DoS in prerouting chain. A value of `0` disables this safeguard      |
| ban_udplimit            | option | 100                           | threshold in number of packets to detect udp DoS in prerouting chain. A value of `0` disables this safeguard      |
| ban_logprerouting       | option | 0                             | log suspicious packets in the prerouting chain                                                                    |
| ban_loginbound          | option | 0                             | log suspicious packets in the inbound chain (wan-input and wan-forward)                                           |
| ban_logoutbound         | option | 0                             | log suspicious packets in the outbound chain (lan-forward)                                                        |
| ban_logratelimit        | option | 10                            | rate (per second) for the shared nft log limit, applied globally across all logged rules                          |
| ban_logburstlimit       | option | 5                             | burst size in packets for the shared nft log limit                                                                |
| ban_autoallowlist       | option | 1                             | add wan IPs/subnets and resolved domains automatically to the local allowlist (not only to the Sets)              |
| ban_autoblocklist       | option | 1                             | add suspicious attacker IPs and resolved domains automatically to the local blocklist (not only to the Sets)      |
| ban_autoblocksubnet     | option | 0                             | add entire subnets to the blocklist Sets based on a rate-limited, non-blocking RDAP lookup for the suspicious IP  |
| ban_autoallowuplink     | option | subnet                        | limit the uplink autoallow function to: `subnet`, `ip` or `disable` it at all                                     |
| ban_allowlistonly       | option | 0                             | restrict the internet access only to specific, explicitly allowed IP segments                                     |
| ban_monitorallowed      | option | 0                             | restrict the log monitor to the entries of the local allowlist (allowlist-only mode)                              |
| ban_allowflag           | option | -                             | always allow certain protocols(tcp or udp) plus destination ports or port ranges, e.g.: `tcp 80 443-444`          |
| ban_allowurl            | list   | -                             | external allowlist feed URLs, one or more references to simple remote IP lists                                    |
| ban_basedir             | option | /tmp                          | base working directory while banIP processing                                                                     |
| ban_reportdir           | option | /tmp/banIP-report             | directory where banIP stores report files                                                                         |
| ban_backupdir           | option | /tmp/banIP-backup             | directory where banIP stores compressed backup files                                                              |
| ban_errordir            | option | /tmp/banIP-error              | directory where banIP stores processing error files                                                               |
| ban_protov4             | option | - / autodetect                | enable IPv4 support                                                                                               |
| ban_protov6             | option | - / autodetect                | enable IPv6 support                                                                                               |
| ban_ifv4                | list   | - / autodetect                | logical wan IPv4 interfaces, e.g. `wan`                                                                           |
| ban_ifv6                | list   | - / autodetect                | logical wan IPv6 interfaces, e.g. `wan6`                                                                          |
| ban_dev                 | list   | - / autodetect                | wan device(s), e.g. `eth2`                                                                                        |
| ban_vlanallow           | list   | -                             | always allow certain VLAN forwards, e.g. br-lan.20                                                                |
| ban_vlanblock           | list   | -                             | always block certain VLAN forwards, e.g. br-lan.10                                                                |
| ban_trigger             | list   | -                             | logical reload trigger interface(s), e.g. `wan` and `wan6`                                                        |
| ban_triggerdelay        | option | 20                            | trigger timeout during interface reload and boot                                                                  |
| ban_deduplicate         | option | 1                             | deduplicate IP addresses across all active Sets (see optional feed flag `dup` below)                              |
| ban_splitsize           | option | 0                             | split the processing/loading of Sets in chunks of n lines/members (saves RAM)                                     |
| ban_cores               | option | - / autodetect                | limit the cpu cores used by banIP; only auto-detection is memory-capped                                           |
| ban_nftloglevel         | option | warn                          | nft loglevel, values: emerg, alert, crit, err, warn, notice, info, debug                                          |
| ban_nftpriority         | option | -100                          | nft priority for the banIP table (the prerouting table is fixed to priority -150)                                 |
| ban_nftpolicy           | option | memory                        | nft policy for banIP-related Sets, values: memory, performance                                                    |
| ban_nftexpiry           | option | -                             | expiry time (ms|s|m|h|d) for auto added blocklist members (also controls the monitor cache refresh interval)      |
| ban_nftretry            | option | 3                             | number of Set load attempts in case of an error                                                                   |
| ban_nftcount            | option | 0                             | enable nft counter for every Set element                                                                          |
| ban_bcp38               | option | 0                             | block packets with spoofed source IP addresses in all supported chains                                            |
| ban_map                 | option | 0                             | enable a GeoIP Map with suspicious Set elements                                                                   |
| ban_feed                | list   | -                             | external download feeds, e.g. `yoyo`, `doh`, `country` or `talos` (see feed table)                                |
| ban_asn                 | list   | -                             | ASNs for the `asn` feed, e.g.`32934`                                                                              |
| ban_asnsplit            | option | -                             | the selected ASNs are stored in separate Sets                                                                     |
| ban_region              | list   | -                             | Regional Internet Registry (RIR) country selection. Supported regions are: AFRINIC, ARIN, APNIC, LACNIC and RIPE  |
| ban_country             | list   | -                             | country iso codes for the `country` feed, e.g. `ru`                                                               |
| ban_countrysplit        | option | -                             | the selected countries are stored in separate Sets                                                                |
| ban_blockpolicy         | option | drop                          | `drop` packets silently on input and forwardwan chains or actively `reject` the traffic                           |
| ban_feedin              | list   | -                             | limit the selected feeds to the inbound chain (wan-input and wan-forward)                                         |
| ban_feedout             | list   | -                             | limit the selected feeds to the outbound chain (lan-forward)                                                      |
| ban_feedinout           | list   | -                             | set the selected feeds to the inbound and outbound chain (lan-forward)                                            |
| ban_feedreset           | list   | -                             | override the default feed configuration and remove existing port/protocol limitations                             |
| ban_feedcomplete        | list   | -                             | opt out the selected feeds from the deduplication process                                                         |
| ban_fetchcmd            | option | - / autodetect                | `uclient-fetch`, `wget` or `curl`                                                                                 |
| ban_fetchparm           | option | - / autodetect                | set the config options for the selected download utility                                                          |
| ban_fetchretry          | option | 5                             | number of download attempts in case of an error (not supported by uclient-fetch)                                  |
| ban_fetchinsecure       | option | 0                             | don't check SSL server certificates during download                                                               |
| ban_mailreceiver        | option | -                             | receiver address for banIP related notification E-Mails                                                           |
| ban_mailsender          | option | no-reply@banIP                | sender address for banIP related notification E-Mails                                                             |
| ban_mailtopic           | option | banIP notification            | topic for banIP related notification E-Mails                                                                      |
| ban_mailprofile         | option | ban_notify                    | mail profile used in `msmtp` for banIP related notification E-Mails                                               |
| ban_mailnotification    | option | 0                             | receive E-Mail notifications with every banIP run                                                                 |
| ban_resolver            | option | -                             | external resolver used for DNS lookups, by default the local resolver/forwarder will be used                      |
| ban_remotelog           | option | 0                             | enable the cgi interface to receive remote logging events                                                         |
| ban_remotetoken         | option | -                             | unique token to communicate with the cgi interface                                                                |

<a id="examples"></a>
## Examples
**banIP report information**

```
/* Detailed source-code truncated for AI context efficiency. */
```

**banIP runtime information**  

```sh
~# /etc/init.d/banip status
::: banIP runtime information
  + status            : active (nft: ✔, monitor: ✔)
  + frontend_ver      : 1.9.0-r1
  + backend_ver       : 1.9.0-r1
  + element_count     : 111 582 (chains: 7, sets: 21, rules: 50)
  + active_feeds      : allowlist.v4MAC, allowlist.v6MAC, allowlist.v4, allowlist.v6, cinsscore.v4, debl.v4, country.v6, debl.v6, doh.v4, doh.v6, spamhaus.v4, ipthreat.v4, country.v4, spamhaus.v6, threat.v4, turris.v4, turris.v6, blocklist.v4MAC, blocklist.v6MAC, blocklist.v4, blocklist.v6
  + active_devices    : trigger: wan / wan: pppoe-wan / wan-if: wan, wan_6 / vlan-allow: - / vlan-block: -
  + active_uplink     : 46.167.33.169, 2a04:5700:304:ea66:e1fa:9a7f:ff78:ec41
  + nft_info          : ver: 1.1.6-r4, priority: -100, policy: performance, loglevel: warn, expiry: 2h, limit (icmp/syn/udp): 25/10/100, loglimit (rate/burst): 10/5
  + run_info          : base: /mnt/data/banIP, backup: /mnt/data/banIP/backup, report: /mnt/data/banIP/report, error: /mnt/data/banIP/error
  + run_flags         : auto: ✔, proto (4/6): ✔/✔, bcp38: ✔, log (pre/in/out): ✘/✘/✘, count: ✔, dedup: ✔, split: ✘, custom feed: ✘, allowed only: ✘, debug: ✔
  + last_run          : mode: restart, date / time: 2026-09-19 15:27:29, duration: 0m 28s, memory: 1122.99 MB available
  + system_info       : cores: 4, log: logread, fetch: curl, Bananapi BPI-R3, mediatek/filogic, OpenWrt SNAPSHOT (r36370-5369699684)
```

**banIP search information**  

```sh
~# /etc/init.d/banip search 8.8.8.8
:::
::: banIP Search
:::
    Looking for IP '8.8.8.8' on 2025-01-13 22:13:36
    ---
    IP found in Set 'country.v4'
    IP found in Set 'doh.v4'
```

**banIP Set content information**  
List all elements of a given Set with hit counters, e.g.:

```sh
~# /etc/init.d/banip content turris.v4
:::
::: banIP Set Content
:::
    List elements of the Set 'turris.v4' on 2025-06-08 23:28:55
    ---
1.4.228.135, packets:  0
1.23.16.3, packets:  0
1.33.35.42, packets:  0
1.33.231.132, packets:  0
1.34.29.158, packets:  0
1.34.231.106, packets:  0
1.52.91.174, packets:  0
1.64.149.142, packets:  0
1.69.243.13, packets:  0
1.70.139.250, packets:  0
1.70.171.246, packets:  0
1.82.191.114, packets:  0
[...]
```

List only elements with hits of a given Set with hit counters, e.g.:
```sh
~# /etc/init.d/banip content turris.v4 true
:::
::: banIP Set Content
:::
    List elements of the Set 'turris.v4' on 2025-06-08 23:30:59
    ---
74.50.211.178, packets:  1
109.205.213.115, packets:  18
109.205.213.123, packets:  35
109.205.213.248, packets:  29
109.205.213.250, packets:  20
109.205.213.252, packets:  30
122.222.152.65, packets:  1
186.91.25.141, packets:  2
190.203.106.113, packets:  2
200.123.238.20, packets:  1
```

<a id="best-practise-and-tweaks"></a>
## Best practise and tweaks
**Recommendation for low memory systems**
nftables supports the atomic loading of firewall rules (incl. elements), which is cool but unfortunately is also very memory intensive. To reduce the memory pressure on low memory systems (i.e. those with 256-512MB RAM), you should optimize your configuration with the following options:

* point `ban_basedir`, `ban_reportdir`, `ban_backupdir` and `ban_errordir` to an external usb drive or ssd
* set `ban_cores` to `1` (only useful on a multicore system) to force sequential feed processing. The autodetected value is additionally capped to the available memory; a manually set value is always used as-is and is never lowered
* set `ban_splitsize` e.g. to `1024` to split the load of an external Set after every 1024 lines/elements
* set `ban_nftcount` to `0` to deactivate the CPU- and memory-intensive creation of counter elements at chain / Set level. With this setting, all packet counters are disabled, the Set Reporting will show zero values for these even when the protection rules are actively dropping traffic. Only the DoS protection counters (`syn-flood`, `udp-flood`, `icmp-flood`, etc.) are always enabled.

**Sensible choice of blocklists**  
The following feeds are just my personal recommendation as an initial setup:
* cinsscore, debl, turris and doh in their default chains

In total, this feed selection blocks about 20K IP addresses. It may also be useful to include some countries to the country feed.
Please note: don't just blindly activate (too) many feeds at once, sooner or later this will lead to OOM conditions.

**Feed direction (inbound vs. outbound) — and the country/ASN case**  
This is the single most common source of confusion, so it's worth understanding the model before you pick feeds.

banIP filters on the **connection level** (source/destination IP), not on content. The default chain of a feed therefore has to match **who initiates the unwanted connection**:

* **Inbound** (WAN-Input + WAN-Forward): blocks connections that come **from the internet towards you**. This is the right direction for the large majority of reputation feeds, which list the **source** IPs of attackers, scanners, brute-forcers and spam senders (`cinsscore`, `debl`, `dshield`, `firehol*`, `bruteforceblock`, `uceprotect*`, …). A fail2ban-style feed only makes sense inbound.
* **Outbound** (LAN-Forward): blocks connections your **own LAN clients initiate towards** a listed IP. This is the right direction for feeds that list destinations your devices should never reach: malware-download hosts, command-and-control servers, DoH/DNS bypass endpoints and threat IPs (`doh`, `dns`, `hagezi`, `feodo`, `urlhaus`, `urlvir`, `webclient`).
* **Both**: feeds where the risk genuinely exists in both directions default to `inout` (`spamhaus`, `threat`, `threatview`, `tor`, `proxy`, `vpn`, `vpndc`).

The classic mistake: enabling a malware-URL / C2 feed like `urlhaus` in the **inbound** chain and expecting it to stop a LAN client from downloading malware. It won't — the client is the one opening the connection, so only the **outbound** chain can intercept it. These feeds ship with sensible defaults (outbound or both), but if you maintain your own custom feeds, keep this rule of thumb in mind.

**The country/ASN case:** the `country` and `asn` feeds default to the **inbound** chain. This blocks traffic *from* the selected country/ASN towards you — the typical "keep attackers from region X out" use case. If your goal is instead to stop your **own clients from reaching** a country or ASN (the recurring "block all connections *to* country X" request), inbound alone does nothing for you. Switch the feed to the outbound or both chain:

* via LuCI: `Feed/Set Settings` → move the feed into `Outbound` or `Inbound + Outbound`
* via CLI in `/etc/config/banip`:

```
# block outbound connections to the selected countries as well as inbound
list ban_feedinout 'country'

# or restrict the country feed exclusively to the outbound (LAN-FWD) chain
list ban_feedout 'country'
```

Be aware that bidirectional country blocking on large countries quickly inflates element counts and memory usage; consider `ban_countrysplit`/`ban_asnsplit` and the low-memory tweaks above.

**Log Terms for logfile parsing**  
Like fail2ban and crowdsec, banIP supports logfile scanning and automatic blocking of suspicious attacker IPs.
In the default config only the log terms to detect failed login attempts via dropbear and LuCI are in place. The following search patterns have been tested as well:

```
dropbear : 'Exit before auth from'
LuCI     : 'luci: failed login'
sshd1    : 'error: maximum authentication attempts exceeded'
sshd2    : 'sshd.*Connection closed by.*\[preauth\]'
asterisk : 'SecurityEvent=\"InvalidAccountID\".*RemoteAddress='
openvpn  : 'TLS Error: could not determine wrapping from \[AF_INET\]'
AdGuard  : 'AdGuardHome.*\[error\].*/control/login: from ip'
Remote   : 'received a suspicious remote IP'
```

**Source IP position**  
For every matched line banIP picks the attacker's address out of the log entry. By default it uses the *last* IP in the line, which is correct for all login-style formats above: the service always appends the real remote address as the final field, after any user-supplied data (such as a login name). This stays robust even when an attacker puts an IP-like string into a username or similar field.

Some formats instead lead with the source IP — most notably web-server access logs, where the client address is the first field. For those, prefix the term with `first:`:

```
nginx : 'first:"[A-Z]+ /wp-login\.php[^"]*" 40[13]'
```

The `first:`/`last:` prefix is parsed off before matching and applies only to that single term, so terms with different anchors can be mixed freely.

Please note: `first:` only fits formats where the source IP is genuinely the first address in the line. Do **not** use it for Suricata fast.log (its leading `[1:2013:7]` signature id) or netfilter `LOG` lines (their leading `MAC=` field), as that leading field would be picked instead of the source — keep those on the default `last`.  

You find the `Log Terms` option in LuCI under the `Log Settings` tab. Feel free to add more log terms to meet your needs and protect additional services.

**Allow-/Blocklist handling**  
banIP supports local allow- and block-lists, MAC/IPv4/IPv6 addresses (incl. ranges in CIDR notation) or domain names. These files are located in /etc/banip/banip.allowlist and /etc/banip/banip.blocklist.
Unsuccessful login attempts or suspicious requests will be tracked and added to the local blocklist (see the `ban_autoblocklist` option). The blocklist behaviour can be further tweaked with the `ban_nftexpiry` option.
Elements added by the log monitor, including the RDAP ranges of an offending address, expire after `ban_nftexpiry`, but every new hit within an existing range resets its timeout. As the Sets use `auto-merge`, overlapping or adjacent ranges are also combined into a single larger element. A network under continuous attack can therefore stay in the blocklist far beyond the configured expiry and grow over time - this is intended behaviour, not a stale entry.
Depending on the options `ban_autoallowlist` and `ban_autoallowuplink` the uplink subnet or the uplink IP will be added automatically to local allowlist.
Entries in CIDR notation always refer to the whole network - set host bits are masked by nftables, e.g. `192.168.1.5/24` ends up as `192.168.1.0/24` in the Set and `2001:db8::e65f:1:2:3/64` as `2001:db8::/64`. To allow or block a single host omit the prefix length or use /32 resp. /128.
Furthermore, you can reference external Allowlist URLs with additional IPv4 and IPv6 feeds (see `ban_allowurl`).
Both local lists also accept domain names as input to allow IP filtering based on these names. The corresponding IPs (IPv4 & IPv6) will be extracted and added to the Sets.

**Allowlist-only mode**  
banIP supports an "allowlist only" mode. This option restricts Internet access only to certain, explicitly permitted IP segments - and blocks access to the rest of the Internet. All IPs that are _not_ listed in the allowlist or in the external allowlist URLs are blocked. In this mode it might be useful to limit the allowlist feed to the inbound chain, to still allow outbound communication to the rest of the world.

In this mode the allowlist Sets define the permitted address scope, they do not act as an accept precedence - the blocklist Set is still evaluated and still applies. By default the log monitor nevertheless treats every allowlisted IP as exempt, therefore attackers coming from a permitted segment (e.g. a country-wide external allowlist feed) are never banned. Enable the `ban_monitorallowed` option to restrict the log monitor to the entries of the local allowlist. banIP then processes an additional local feed which provides the `allowlist.local.v4`/`allowlist.local.v6` Sets, built from /etc/banip/banip.allowlist only - including the automatically added uplink. These Sets are not referenced by any rule, they are solely used by the log monitor: local entries stay exempt, IPs which are merely covered by an external allowlist feed can be banned. This allows a "permit my own country, but still screen it" setup. The option has no effect unless `ban_allowlistonly` is enabled, and the additional Sets are removed automatically as soon as it is switched off.

External feeds are skipped in this mode as far as they are redundant: the allowlist emits an inverted and therefore terminal drop rule in every chain it covers, so a feed rule in exactly these chains can never match. With the default allowlist direction all external feeds are skipped. If the allowlist is limited to a single chain - e.g. to the inbound chain - only the feeds of that chain are skipped, feeds which also cover the opposite chain are still processed.

**MAC/IP-binding**  
banIP supports concatenation of local MAC addresses/ranges with IPv4/IPv6 addresses, e.g. to enforce dhcp assignments or to free connected clients from outbound blocking.
The following notations in the local allow- and block-list are supported:

```
MAC-address only:
C8:C2:9B:F7:80:12                                  => this will be populated to the v4MAC- and v6MAC-Sets with the IP-wildcards 0.0.0.0/0 and ::/0

MAC-address range:
C8:C2:9B:F7:80:12/24                               => this populate the MAC-range C8:C2:9B:00:00:00", "C8:C2:9B:FF:FF:FF to the v4MAC- and v6MAC-Sets with the IP-wildcards 0.0.0.0/0 and ::/0

MAC-address with IPv4 concatenation:
C8:C2:9B:F7:80:12 192.168.1.10                     => this will be populated only to v4MAC-Set with the certain IP, no entry in the v6MAC-Set

MAC-address with IPv6 concatenation:
C8:C2:9B:F7:80:12 2a02:810c:0:80:a10e:62c3:5af:f3f => this will be populated only to v6MAC-Set with the certain IP, no entry in the v4MAC-Set

MAC-address with IPv4 and IPv6 concatenation:
C8:C2:9B:F7:80:12 192.168.1.10                     => this will be populated to v4MAC-Set with the certain IP
C8:C2:9B:F7:80:12 2a02:810c:0:80:a10e:62c3:5af:f3f => this will be populated to v6MAC-Set with the certain IP

MAC-address with IPv4 and IPv6 wildcard concatenation:
C8:C2:9B:F7:80:12 192.168.1.10                     => this will be populated to v4MAC-Set with the certain IP
C8:C2:9B:F7:80:12                                  => this will be populated to v6MAC-Set with the IP-wildcard ::/0
```

**MAC-address logging in nftables**  
The MAC-address logging format in nftables is a little bit unusual. It is generated by the kernel's NF_LOG module and places all MAC-related data into one flat field, without separators or labels. For example, the field MAC=7e:1a:2f:fc:ee:29:68:34:21:1f:a7:b1:08:00 is actually a concatenation of the following:

```
[Source MAC (6 bytes)] + [Destination MAC (6 bytes)] + [EtherType (2 bytes)]
7e:1a:2f:fc:ee:29 → the source MAC address
68:34:21:1f:a7:b1 → the destination MAC address
08:00 → the EtherType for IPv4 (0x0800)
```
**BCP38**  
BCP38 (**B**est **C**urrent **P**ractice, RFC 2827) defines ingress filtering to prevent IP address spoofing. In practice, this means:
* dropping packets arriving on the WAN whose source address is not valid or routable via that interface
* dropping packets leaving LAN => WAN whose source address does not belong to the local/internal prefixes

In banIP, the BCP38 implementation uses nftables’ FIB lookup to enforce this. It checks whether the packet’s source address is not valid for the incoming interface or whether the routing table reports no route for this source on this interface. Packets that fail this check are dropped.

**Reporting Counter behavior**  
The `ban_nftcount` option globally controls all Reporting counters in the banIP table — both per-Set element counters and chain rule counters. Default is `0` (disabled) to keep memory usage and CPU overhead low on constrained devices. Only the DoS protection counters (`cnt_synflood`, `cnt_udpflood`, `cnt_icmpflood`, `cnt_tcpinvalid`, `cnt_ctinvalid`, `cnt_bcp38`) become always populated and visible in `/etc/init.d/banip report`.

When enabled (`ban_nftcount=1`):
* every Set element gets its own packet/byte counter (memory cost: ~16 bytes per element, so ~1.6 MB for a 100k-element feed)
* all chain rules count packets and bytes
* the GeoIP Map (`ban_map=1`) becomes available, as it requires per-element counters to identify hit IPs

When disabled (`ban_nftcount=0`):
* memory footprint is minimal, suitable for low memory routers
* the Set Reporting still works structurally, but packet counts will all read as zero
* the GeoIP Map cannot be enabled

For most modern routers with ≥512 MB RAM, enabling `ban_nftcount` is recommended for full visibility into what banIP is actually blocking.

**Log Limit / Options**  
All log rules (prerouting flood protection, inbound and outbound feeds) share a single nft limit named `loglimit`. The default of 10/second with burst 5 is intentionally aligned with typical kernel printk rate limits, so banIP log messages are not dropped by the kernel. Tune via `ban_logratelimit` and `ban_logburstlimit` accordingly if you have adjusted the kernel defaults. Set the `ban_logratelimit` to `0` to disable rate limiting entirely, e.g. when using ulogd or other userspace log handlers without printk constraints.

**Set reporting, enable the GeoIP Map**  
banIP includes a powerful reporting tool on the Set Reporting tab which shows the latest NFT banIP Set statistics. To get the latest statistics always press the "Refresh" button.
In addition to a tabular overview banIP reporting includes a GeoIP map in a modal popup window/iframe that shows the geolocation of your own uplink addresses (in green) and the locations of potential attackers (in red). To enable the GeoIP Map set the following options (in "Feed/Set Settings" config tab):

* set `ban_nftcount` to `1` to enable the nft counter for every Set element
* set `ban_map` to `1` to include the external components listed below and activate the GeoIP map

To make this work, banIP uses the following external components:
* [Leaflet](https://leafletjs.com/) is a lightweight open-source JavaScript library for interactive maps
* The free and quite fast [IP Geolocation API](https://ip-api.com/) to resolve the required IP/geolocation information

The basemap is no longer pulled from a tile service. CARTO started to require an API key for the raster basemaps at basemaps.cartocdn.com and watermarks every unauthenticated tile request, and a key is bound to a single customer, so it cannot be shipped with a package that lands on every installation. banIP therefore draws the basemap from country outlines that come with `luci-app-banip`: [Natural Earth](https://www.naturalearthdata.com) 1:110m, public domain, stripped of all attributes and simplified to 36 kB. No tile service is contacted anymore, but the map page still loads the Leaflet library from unpkg.com (integrity-checked via SRI and requested without cookies), so it needs a WAN connection. The outlines are enough to locate an IP, so the map does not zoom in beyond level 6 and labels the continents rather than the countries.

**Optional: a higher detail basemap**

The shipped 1:110m outlines are coarse around Scandinavia, the Greek islands and the smaller island states. If you want sharper coastlines, build the 1:50m variant with [mapshaper](https://github.com/mbloch/mapshaper) and drop it next to the shipped file. LuCI looks for it on every map run and falls back to the shipped outlines when it is missing, no config option is involved:

```
curl -sSLo ne50.geojson https://raw.githubusercontent.com/nvkelso/natural-earth-vector/v5.1.2/geojson/ne_50m_admin_0_countries.geojson
mapshaper ne50.geojson -filter-fields -simplify 5% keep-shapes -o force precision=0.01 format=geojson world-50m.json
scp world-50m.json root@openwrt:/www/luci-static/resources/view/banip/
```

The result is roughly 105 kB, about three times the shipped file. Please note: this file is not part of any package, so it is removed on sysupgrade unless you add its path to `/etc/sysupgrade.conf`, and it stays behind when `luci-app-banip` is uninstalled.

Please note: the free ip-api.com batch endpoint is rate limited to 15 requests per minute per source IP. Requests beyond that limit are throttled with HTTP 429, and constantly exceeding the limit gets the IP banned for an hour — in both cases the affected part of the map stays empty and banIP logs an info message. To stay below the limit banIP collects the top listed elements of all Sets, deduplicates them and resolves them in batch requests of 100 IPs each (the maximum the endpoint accepts), capped at 15 requests per report run. Elements beyond ~1500 are left out of the map and banIP logs an info message. A report regenerated within 60 seconds of the last geo lookup reuses its map data instead of querying the service again. Please note that other clients behind the same public IP count against the same limit.

**CGI interface to receive remote logging events**  
banIP ships a basic cgi interface in `/www/cgi-bin/banip` to receive remote logging events (disabled by default). The cgi interface evaluates logging events via GET or POST request (see examples below). To enable the cgi interface set the following options:

* set `ban_remotelog` to `1` to enable the cgi interface
* set `ban_remotetoken` to a secret transfer token, allowed token characters consist of '[A-Za-z]', '[0-9]', '.' and ':'
* add the remote logging event to the logterm

Examples to transfer remote logging events from an internal server to banIP via cgi interface:

* POST request: `curl --insecure --data "<ban_remotetoken>=<suspicious IP>" https://192.168.1.1/cgi-bin/banip`
* GET request: `wget --no-check-certificate https://192.168.1.1/cgi-bin/banip?<ban_remotetoken>=<suspicious IP>`

Please note: for security reasons use this cgi interface only internally and only encrypted via https transfer protocol.

**Download options**  
By default banIP uses the following pre-configured download options:

```
    * curl: --connect-timeout 20 --speed-time 20 --retry-delay 10 --retry 4 --retry-all-errors --fail --silent --globoff --show-error --location -o
    * wget: --no-cache --no-cookies --timeout=20 --waitretry=10 --tries=5 --retry-connrefused -O
    * uclient-fetch: --timeout=20 -O
```

The retry-related values shown above are derived from `ban_fetchretry` (default `5`): for curl `--retry` is `ban_fetchretry - 1`, for wget `--tries` equals `ban_fetchretry`. To override the defaults set `ban_fetchretry`, `ban_fetchinsecure` or globally `ban_fetchparm` to your needs.

**Configure E-Mail notifications via `msmtp`**  
To use the email notification you must install and configure the package `msmtp`.
Modify the file `/etc/msmtprc`, e.g.:

```
[...]
defaults
auth            on
tls             on
tls_certcheck   off
timeout         5
syslog          LOG_MAIL
[...]
account         ban_notify
host            smtp.gmail.com
port            587
from            <address>@gmail.com
user            <gmail-user>
password        <password>
```

Finally add a valid E-Mail receiver address in banIP.

**Send status E-Mails and update the banIP lists via cron job**  
For a regular, automatic update of the used feeds or other regular banIP tasks (e.g. status mailing) set up a cron job. Use `reload` here — `start`/`restart` would only restore the backups instead of fetching fresh feeds. In LuCI you find the cron settings under `System` => `Scheduled Tasks`. On the command line the cron file is located at `/etc/crontabs/root`:  

```
55 03 * * * /etc/init.d/banip report mail
00 04 * * * /etc/init.d/banip reload
```

**Redirect asterisk security logs to syslog/logread**  
By default banIP scans the logfile via logread, so to monitor attacks on asterisk, its security log must be available via logread. To do this, edit `/etc/asterisk/logger.conf` and add the line `syslog.local0 = security`, then run `asterisk -rx reload logger` to update the running asterisk configuration.

**Change/add banIP feeds and set optional feed flags**  
The banIP default blocklist feeds are stored in an external JSON file `/etc/banip/banip.feeds`. This file is shipped with the package and is **overwritten on every package update**, so never edit it directly. All of your custom changes belong in the separate JSON file `/etc/banip/banip.custom.feeds` (empty by default), which is preserved across updates. It's recommended to use the LuCI based Custom Feed Editor (`Custom Feed Editor`tab), which validates the JSON for you.  

A feed is a single JSON object, keyed by a unique feed name (no spaces, no special characters). Example:

```json
	[...]
	"doh":{
		"url_4": "https://raw.githubusercontent.com/dibdot/DoH-IP-blocklists/master/doh-ipv4.txt",
		"url_6": "https://raw.githubusercontent.com/dibdot/DoH-IP-blocklists/master/doh-ipv6.txt",
		"rule": "feed 1",
		"chain": "out",
		"descr": "public DoH-Server",
		"flag": "tcp udp 80 443"
	},
	[...]
```

The object supports the following fields:

| Field   | Required | Description                                                                                                              |
| :------ | :------: | :----------------------------------------------------------------------------------------------------------------------- |
| url_4   | yes\*    | download URL of the IPv4 list. \*at least one of `url_4`/`url_6` must be present                                         |
| url_6   | yes\*    | download URL of the IPv6 list. May point to the **same** URL as `url_4` if the source mixes IPv4 and IPv6 in one file    |
| rule    | yes      | the parsing ruleset, max. 4 space separated parameters (see below)                                                       |
| chain   | yes      | the default chain/direction: `in`, `out` or `inout` (see below)                                                          |
| descr   | yes      | a short human-readable description shown in LuCI and the feed table                                                      |
| flag    | no       | optional, space separated list of extra options: archive format and/or protocol/port limitations (see below)             |

**The `url_4` / `url_6` fields**  
Each address family is fetched and processed independently. Three cases:
* IPv4-only source: set `url_4` only, omit `url_6`
* separate IPv4 and IPv6 files: set both to their respective URLs (e.g. `doh`, `spamhaus`)
* a single dual-stack file that mixes v4 and v6 entries: point both `url_4` and `url_6` at that same URL. banIP fetches it only once and process it for each family and the per-family regex extracts the matching addresses; the non-matching lines are simply ignored. (e.g. `threatview`, which ships v4 and v6 in one file).

The `country` and `asn` feeds are special: banIP fetches one file per selected country code / ASN, so their URLs are templates. The placeholder is replaced by the configured value as-is, so country codes have to be lowercase, just like in banip.countries. The placeholder `{country}` is replaced by the lowercase ISO country code, `{asn}` by the bare AS number, e.g. `https://www.ipdeny.com/ipblocks/data/aggregated/{country}-aggregated.zone` or `https://asn.ipinfo.app/api/text/list/AS{asn}`. A URL without placeholder is treated as a base path and gets the legacy suffix appended (`<cc>-aggregated.zone` resp. `AS<asn>`), so older custom feed files keep working. The placeholder can sit anywhere in the URL, which allows to switch the country source to a differently structured one. Example for a GeoLite2-based alternative, which ships IPv4 and IPv6 in one file per country:

```json
	"country":{
		"url_4": "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/text/{country}.txt",
		"url_6": "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/text/{country}.txt",
		"rule": "feed 1",
		"chain": "in",
		"descr": "country blocks (GeoLite2)"
	},
```

Pointing `url_4` and `url_6` at the same file is the dual-stack case described above, so each country file is downloaded only once and processed for both address families. This source includes GeoLite2 data created by MaxMind, available from https://www.maxmind.com - its use is subject to the MaxMind GeoLite End User License Agreement.

Keep in mind that alternative sources come with their own license terms, which you have to check yourself, and that they differ in semantics (geolocation vs. RIR allocation), granularity (set sizes and memory footprint may grow considerably) and code coverage (a missing country file only logs a download failure for that code).

**The `rule` field**  
The rule consists of max. 4 individual, space separated parameters:
1. **type**: `feed` or `suricata` (required)
   * `feed`: a plain IP/CIDR list, one entry per line (the common case)
   * `suricata`: a Suricata/Snort-style ruleset; banIP extracts the IPs out of the rule lines
2. **prefix**: an optional search term (a literal string, not a regex) that a line must contain to be considered a valid entry. Use it to skip comment/header lines or to pick only the relevant rows. Omit it if every data line is a bare IP.
3. **column**: the 1-based column that holds the IP within a matching line, e.g. `1` for a bare list or `13` for the dshield block file (required)
4. **separator**: an optional field separator; default is the whitespace character class `[[:space:]]`. Pass a literal character such as `,` for comma-separated sources (e.g. turris).

**The `chain` field**  
Defines the default blocking direction. See the [Best practise feed-direction note](#best-practise-and-tweaks) for the reasoning behind each value:
* `in`: inbound only (WAN-Input + WAN-Forward). Use for source-IP reputation feeds (attackers, scanners, spam senders).
* `out`: outbound only (LAN-Forward). Use for destination feeds your own clients should never reach (malware hosts, C2, DoH/DNS endpoints).
* `inout`: both directions. Use when the risk genuinely exists on both sides (e.g. spamhaus, tor, proxy, vpn).

This is only the **default**; a user can always override it at runtime per feed via `ban_feedin` / `ban_feedout` / `ban_feedinout`, or strip a feed's port/protocol limitation via `ban_feedreset` — without touching the feed JSON.

**The `flag` field (optional)**  
A space separated list of extra options:
* `gz`: the source is gzip-compressed and will be decompressed before parsing (e.g. `backscatterer`, `ipthreat`, `uceprotect*`)
* protocol/port limitation — one or both of `tcp` / `udp` followed by one or more destination ports or port ranges. This restricts the feed's rules to those destination ports, which is mainly useful for outbound feeds to cut false positives.  
Examples:  
  * `tcp udp 80 443`: limit to HTTP/HTTPS (e.g. `doh`, `hagezi`, `feodo`, `urlhaus`, `urlvir`, `webclient`)
  * `tcp udp 53 853`: limit to plain DNS and DNS-over-TLS (e.g. `dns`)
  * `tcp 22`: limit to SSH
  * `tcp 5060-5061 udp 5060`: mix of single ports and ranges
  * `gz` and a port limitation can be combined, e.g. `gz tcp udp 80 443`

After editing `/etc/banip/banip.custom.feeds`, reload banIP (`/etc/init.d/banip reload`) and check the `Processing Log` tab — a malformed JSON object or a wrong column/separator typically shows up there as a feed that loads zero elements.

<a id="troubleshooting-and-debug-options"></a>
## Troubleshooting & debug options
banIP provides an optional debug mode that writes diagnostic information to the system log and captures internal error output in a dedicated error logfile - by default located in the banIP base directory as `/tmp/ban_error.log`. The log file is automatically cleared at the beginning of each run. Under normal conditions, all error messages are discarded to keep regular runs clean and silent.

Whenever you encounter banIP related processing problems, please enable `Verbose Debug Logging`, restart banIP and check the `Processing Log` tab.
Typical symptoms:
* The nftables initialization failed: untick the `Auto Detection` option in the `General Settings` config section and set the required device and tools options manually
* A blocklist feed does not work: maybe a temporary server problem or the download URL has been changed. In the latter case, just use the Custom Feed Editor to point this feed to a new URL

In case of a nft processing error, banIP creates an error directory (by default `/tmp/banIP-error`) with the faulty nft load files.
For further troubleshooting, you can try to load such an error file manually to determine the exact cause of the error, e.g.: `nft -f error.file.nft`.

Whenever you encounter firewall problems, enable the logging of certain chains in the `Log Settings` config section, restart banIP and check the `Firewall Log` tab.
Typical symptoms:
* A feed blocks a legit IP: disable the entire feed or add this IP to your local allowlist and reload banIP
* A feed (e.g. doh) interrupts almost all client connections: check the feed table above for reference and reset the feed to the defaults in the "Feed/Set Settings" config tab section
* The allowlist doesn't free a certain IP/MAC address: check the current content of the allowlist with the "Set Content" under the "Set Reporting" tab to make sure that the desired IP/MAC is listed - if not, reload banIP

<a id="support"></a>
## Support
Please join the banIP discussion in this [forum thread](https://forum.openwrt.org/t/banip-support-thread/16985) or contact me by mail <dev@brenken.org>
If you want to report an error, please describe it in as much detail as possible - with (debug) logs, the current banIP status, your banIP configuration, etc.

<a id="removal"></a>
## Removal
Stop all banIP related services with `/etc/init.d/banip stop` and remove the banip package if necessary.

<a id="donations"></a>
## Donations
You like this project - is there a way to donate? Generally speaking "No" - I have a well-paying full-time job and my OpenWrt projects are just a hobby of mine in my spare time.

If you still insist to donate some bucks ...
* I would be happy if you put your money in kind into other, social projects in your area, e.g. a children's hospice
* Let's meet and invite me for a coffee if you are in my area, the “Markgräfler Land” in southern Germany or in Switzerland (Basel)
* Send your money to my [PayPal account](https://www.paypal.me/DirkBrenken) and I will collect your donations over the year to support various social projects in my area

No matter what you decide - thank you very much for your support!

Have fun!  
Dirk

---

### Overview (`net/etherwake-nfqueue/README.md`)

# OpenWrt package feed for etherwake-nfqueue


## Wake up computers on netfilter match

This repository contains the OpenWrt package feed for
[etherwake-nfqueue](https://github.com/mister-benjamin/etherwake-nfqueue),
a fork of the **etherwake** Wake-on-LAN client, with support to send magic
packets only after a queued packet is received from the Linux *nfnetlink_queue*
subsystem.

When running **etherwake-nfqueue** on a residential gateway or other type of
router, it can wake up hosts on its network based on packet filtering rules.

For instance, when your set-top box wants to record a TV programme and
tries to access a network share on your NAS, which is in sleep or standby mode,
**etherwake-nfqueue** can wake up your NAS. Or when you set up port forwarding
to a host on your home network, **etherwake-nfqueue** can wake up your host
when you try to access it over the Internet.

The documentation below is mostly OpenWrt specific. For more information on
etherwake-nfqueue itself and use case examples, please consult its
[Readme](https://github.com/mister-benjamin/etherwake-nfqueue/blob/master/README.md).


## Building the package

Currently, no pre-built packages are provided. The following assumes that you
already have a working OpenWrt build system for your target device.

If you haven't, you can follow one of these guides:

* If you only want to compile packages, and no firmware image:
  [Build system – Installation](https://openwrt.org/docs/guide-developer/build-system/install-buildsystem) and
  [Using the SDK](https://openwrt.org/docs/guide-developer/using_the_sdk)

* To quickly build a firmware image off a development snapshot:
  [Quick Image Building Guide](https://openwrt.org/docs/guide-developer/quickstart-build-images)

* Or when you are completely new to using build systems:
  [Beginners guide to building your own firmware](https://openwrt.org/docs/guide-user/additional-software/beginners-build-guide)

### Dependencies

**etherwake-nfqueue** depends on these OpenWrt packages:

* libnetfilter-queue
* iptables-mod-nfqueue

They will be automatically selected and compiled for you. If they are not
installed on your target device, *opkg* will try to resolve dependencies with
packages in the repositories.

### Adding the package feed

First, you need to add the **etherwake-nfqueue** package feed to your build
system.  In the root directory of your OpenWrt build system, find the file
*feeds.conf* (or *feeds.conf.default* if the former shouldn't exist) and add
the following line to it:

```
src-git ethernfq https://github.com/mister-benjamin/etherwake-nfqueue-openwrt.git
```

Then update and install the package feed:
```
user@host:~/openwrt$ scripts/feeds update ethernfq
user@host:~/openwrt$ scripts/feeds install -a -p ethernfq
```

After that, enter OpenWrt's configuration menu
```
user@host:~/openwrt$ make menuconfig
```
and enable **etherwake-nfqueue** in the **Network** sub-menu. It can either be selected
as built-in (\*) or module (M), depending on your decision to include it into a
firmware image or just build the *opkg* package for installation.

Then you should be able to compile the package:
```
user@host:~/openwrt$ make package/etherwake-nfqueue/compile
```

The path of the resulting package depends on your selected *Target System*.
In case of the Linksys WRT1200AC, it can be found here:
```
bin/packages/arm_cortex-a9_vfpv3/ethernfq/etherwake-nfqueue_2019-09-10-67e9d4ca-1_arm_cortex-a9_vfpv3.ipk
```


## Installation

One way to install the package is by simply copying it over to the device with *scp*:
```
user@host:~$ scp etherwake-nfqueue_2019-09-10-67e9d4ca-1_arm_cortex-a9_vfpv3.ipk root@gateway:~
```
And then, install it on the device:
```
root@gateway:~# opkg install etherwake-nfqueue_2019-09-10-67e9d4ca-1_arm_cortex-a9_vfpv3.ipk
```


## Configuration

### WoL Targets

After a fresh installation, no target is configured. Targets are referred
to as the hosts to wake up. Multiple targets can coexist.

Targets can be configured with OpenWrt's UCI.

For example, to add a target called **nas**, with MAC address
 **00:25:90:00:d5:fd**, which is reachable over the VLAN configured
on **eth0.3**, issue this command sequence on your router:

```
uci add etherwake-nfqueue target
uci set etherwake-nfqueue.@target[-1].name=nas
uci set etherwake-nfqueue.@target[-1].mac=00:25:90:00:d5:fd
uci set etherwake-nfqueue.@target[-1].interface=eth0.3
uci commit
```

For each target, one instance of **etherwake-nfqueue** will be started.

Each instance should bind to a different *nfnetlink_queue*. A queue can
be referenced by its queue number. Counting starts from 0, which is the default.
To use a different queue, provide the **nfqueue_num** option. The
following could have been added to the sequence above to use queue 1 instead
of 0:

```
uci set etherwake-nfqueue.@target[-1].nfqueue_num=1
```

The necessity of a queue number will probably become clear, when the iptables
rules are configured in section [Setup firewall rules](#setup-firewall-rules).

The full list of options for a target is:

| Option      | Required | Description                                      |
| ----------- | -------- | ------------------------------------------------ |
| name        | no       | Name of the target, e.g. name=example            |
| mac         | yes      | MAC address of the host to wake up, e.g. mac=00:22:44:66:88:aa |
| nfqueue_num | no       | The queue number used for receiving filtered packets, default is nfqueue_num=0 |
| interface   | no       | The interface used for sending the magic packet, default is interface=eth0 |
| broadcast   | no       | Send magic packet to broadcast address, default is broadcast=off |
| password    | no       | Set a password (required by some adapters), e.g. password=00:22:44:66:88:aa or 192.168.1.1 |
| enabled     | no       | Optionally disable the target, default is enabled=true |

After committing your changes, the settings are persisted to
*/etc/config/etherwake-nfqueue*. This is an illustrative example:
```
config etherwake-nfqueue 'setup'
        option sudo 'off'
        option debug 'off'

config target
        option name 'nas'
        option mac '00:25:90:00:d5:fd'
        option interface 'eth0.3'

config target
        option name 'xyz-board'
        option mac '00:25:90:00:d5:fc'
        option nfqueue_num '1'
        option enabled 'false'

config target
        option name 'ip-camera'
        option mac '00:25:90:00:d5:fb'
        option nfqueue_num '2'
        option interface 'eth0.3'
        option broadcast 'on'
        option password '00:25:90:00:d5:fb'
```

When all target(s) are configured, restart the *etherwake-nfqueue* service:
```
/etc/init.d/etherwake-nfqueue restart
```

### Setting up filters

Without any firewall rules which tell the kernel to match and add packets
to a *nfnetlink_queue*, **etherwake-nfqueue** will never send out a magic
packet to wake its target.

#### Prerequisites

In order to let the *netfilter* framework of the kernel see the packets,
they need to pass through the router. This is usually not the case when
hosts are on the same subnet and don't require network layer routing.
The data will only pass through the router's switch on the link layer.

As a consequence, we can only use packets as a trigger which need to be
routed or bridged by the router. Packets being forwarded between WAN
and LAN are of that type. For other SOHO use cases, partitioning your
network by means of subnets or VLANs might be necessary. The latter
is often used to set up a DMZ.

For VLANs:

* There's a mini howto referring to the **LuCI Web Interface**
  *(Network -> Switch)* way of configuring VLANs:
  [How-To: Creating an additional virtual switch on a typical home router](https://openwrt.org/docs/guide-user/network/vlan/creating_virtual_switches)

* The manual approach is documented here:
  [VLAN](https://openwrt.org/docs/guide-user/network/vlan/switch_configuration)

Guides to setup a DMZ can be found here:

* [Guide to set up DMZ via LUCI](https://forum.openwrt.org/t/guide-to-set-up-dmz-via-luci/21616)

* [fw3 DMZ Configuration Using VLANs](https://openwrt.org/docs/guide-user/firewall/fw3_configurations/fw3_dmz)

The physical switch layout is device specific. E.g. the layout for the Linksys
WRT AC Series is documented
[here](https://oldwiki.archive.openwrt.org/toh/linksys/wrt_ac_series#switch_layout).


Using two LANs or VLANs with the same network address and bridging them again
is a trick to setup a transparent (or bridging) firewall on the same subnet.
This way, packets can be seen by *netfilter* on the router even if the
packets are not routed. Unfortunately this doesn't help when the host
which we want to wake up is offline, as the ARP requests for the destination
IP address are not answered and thus the client trying to reach out to its
destination will not send any *network layer* packets. We could use *arptables*
instead to wake the host when someone requests its MAC address, but this
would probably happen too often and no fine-grained control would be possible.

As a workaround, it might be possible to configure a static ARP entry on your
router (untested), e.g. with:
```
ip neigh add 192.168.0.10 lladdr 00:25:90:00:d5:fd nud permanent dev eth0.3
```
Note that this requires the *ip-full* OpenWrt package to be installed.

To make your firewall rules work with bridging, you need to install the
*kmod-br-netfilter* package and add `net.bridge.bridge-nf-call-iptables=1`
to */etc/sysctl.conf*.


#### Setup firewall rules

One way to setup custom firewall rules in OpenWrt is through its
*/etc/firewall.user* script. This file can also be edited by means of
the **LuCI Web Interface** *(Network -> Firewall -> Custom Rules)*.

The file is interpreted as a shell script, so we can simply use **iptables**
to add our custom firewall rules.

Notice the comment
```
# Internal uci firewall chains are flushed and recreated on reload, so
# put custom rules into the root chains e.g. INPUT or FORWARD or into the
# special user chains, e.g. input_wan_rule or postrouting_lan_rule.
```

Refer to [Packet flow](https://oldwiki.archive.openwrt.org/doc/uci/firewall#packet_flow)
for usable chains. In the example below, the chains *forwarding_lan_rule* and
*forwarding_wan_rule* are used. To inspect the rule sets of the different tables, one can
use

```
iptables --list                  # default is --table filter
iptables --table nat --list
iptables --table mangle --list
iptables --table raw --list      # requires kmod-ipt-raw
```

The following is an example of what could be added to */etc/firewall.user*:

```
/* Detailed source-code truncated for AI context efficiency. */
```

In this example, packets are filtered based on the protocol, their input
and output interfaces, their destination (IP address) and their destination
port(s).

The option `--match conntrack --ctstate NEW` only matches packets of a new
connection and `--match limit --limit 3/hour --limit-burst 1` limits the
amount of packets that are matched. The latter option roughly matches
only one packet per 20 minutes. The intention here is to not be too intrusive
and avoid sending a lot of magic packets.

The `--jump NFQUEUE --queue-num 0` options tell the *netfilter*
framework to enqueue a matching packet to the NFQUEUE number 0. In this
example, all four rules send the matching packets into queue 0. The
additional option `--queue-bypass` helps in the situation, when
**etherwake-nfqueue** isn't running. Packets will then be handled
as if the rule wasn't present.


## Disabling targets

To disable targets, first find their index:
```
uci show etherwake-nfqueue
```

Then set its *enabled* option to false and restart the service.
For index 0, it can be done like this:
```
uci set etherwake-nfqueue.@target[0].enabled=false
/etc/init.d/etherwake-nfqueue restart
```


## Troubleshooting

### Debug mode

In order to see what's going on in syslog and get some debug output when
starting the service, enable etherwake-nfqueue's debug mode:
```
uci set etherwake-nfqueue.setup.debug=on
```
In another user session tail the log:
```
logread -f
```
And then restart the service:
```
/etc/init.d/etherwake-nfqueue restart
```

### Inspect netfilter

To inspect the working of your firewall rules, you can print statistics
of the chains you used, e.g.:
```
iptables --verbose --list forwarding_lan_rule
```

If you happen to have the *procps-ng-watch* package installed, you can watch
them:
```
watch iptables --verbose --list forwarding_lan_rule
```

To see, if your queues are in place, use:
```
cat /proc/net/netfilter/nfnetlink_queue
```

## Potential improvements

* Add **LuCI Web Interface** configuration frontend for *targets* and *filter rules*
* Add an option to set *nice* values for instances

---

### Overview (`net/family-dns/Readme.md`)

# Package: net/family-dns

This package configures your router to block access to adult websites.

The default settings are designed to be appropriate for most businesses, schools, and families.

Enabled and disable Family DNS by editing /etc/config/family-dns. To make
your changes active, run family-dns-update.

- Default DNS Filter
    - CleanBrowsing.org Adult Filter
        -  https://cleanbrowsing.org/filters#adult
        - Blocks access to all adult, pornographic and explicit sites. It does not block proxy or VPNs, nor mixed-content sites. Sites like Reddit are allowed. Google and Bing are set to the Safe Mode. Malicious and Phishing domains are blocked.
- Alternate DNS Filters:
    - CleanBrowsing.org Family Filter
        -  https://cleanbrowsing.org/filters#family
        - Blocks access to all adult, pornographic and explicit sites. It also blocks proxy and VPN domains that are used to bypass the filters. Mixed content sites (like Reddit) are also blocked. Google, Bing and Youtube are set to the Safe Mode. Malicious and Phishing domains are blocked.
	- Cisco Family Shield
		- https://www.opendns.com/home-internet-security/
		- https://www.opendns.com/setupguide/#familyshield
		- FamilyShield will block domains that are categorized as: Tasteless, Proxy/Anonymizer, Sexuality and Pornography.

---

### Overview (`net/geoip-shell/OpenWrt-README.md`)

## geoip-shell on OpenWrt

Currently geoip-shell fully supports OpenWrt, both with firewall3 + iptables and with firewall4 + nftables, while providing the same user interface and features as on any other Linux system. So usage is the same as described in the main [README.md](README.md) file, while some parts of the backend (namely persistence implementation), some defaults and the location of the data directory are different.

The _geoip-shell-iptables_ package is for firewall3+iptables OpenWrt systems, while the _geoip-shell_ package is for firewall4+nftables OpenWrt systems.

A LuCi interface has not been implemented (yet). As on any other Linux system, all user interface is via a command line (but my goal is to make this an easy experience regardless). If this discourages you from using geoip-shell, please let me know. A few people asking for this feature may motivate me to prioritize it.

## Usage after installation via ipk
After installing the ipk package, geoip-shell will be inactive until you configure it. To do so, run `geoip-shell configure` and follow the interactive setup. You can also run `geoip-shell -h` before that to find out about configuration options and then append certain options after the `configure` action, for example: `geoip-shell configure -c "de nl" -m whitelist` to configure geoip-shell in whitelist mode for countries Germany and Netherlands. The interactive setup will ask you about all the important options but some niche options are only available non-interactively (for example if you want to configure geoblocking for certain selection of ports). You can always change these settings after initial configuration via the same `geoip-shell configure` command.

## Uninstallation of geoip-shell if installed via ipk
- For nftables-based systems: `opkg remove geoip-shell`
- For iptables-based systems: `opkg remove geoip-shell-iptables`

## Resources management on OpenWrt
Because OpenWrt typically runs on embedded devices with limited memory and very small flash storage, geoip-shell implements some techniques to conserve these resources as much as possible:
- During installation on OpenWrt, comments and the debug code are stripped from the scripts to reduce their size.
- Only the required modules are installed, depending on the system (iptables- or nftables- based).
- I've researched the most memory-efficient way for loading ip lists into nftables sets. Currently, nftables has some bugs related to this process which may cause unnecessarily high memory consumption. geoip-shell works around these bugs as much as possible.
- To avoid unnecessary flash storage wear, all filesystem-related tasks geoip-shell does which do not require permanent storage are done in the /tmp directory which in the typical OpenWrt installation is mounted on the ramdisk.
- Some defaults on OpenWrt are different to further minimize flash storage wear (read below).

### Scripts size
Typical geoip-shell installation on an OpenWrt system currently consumes around 120kB. The distribution folder itself weighs quite a bit more (mainly because of documentation) but you can install via an ipk which doesn't remain in storage after installation, or if installing via the -install script, delete the distribution folder and free up space taken by it. geoip-shell does not install its documentation into the system.
I have some plans to reduce that size by compressing certain scripts which provide user interface and implementing automatic extraction to /tmp when the user wants to access them, but this is not yet implemented.

To view all installed geoip-shell scripts in your system and their sizes, run `ls -lh /usr/bin/geoip-shell-* /usr/lib/geoip-shell/*`.

## Persistence on OpenWrt
- Persistence of geoip firewall rules and ip sets works differenetly on OpenWrt than on other Linuxes, since geoip-shell has an OpenWrt-specific procd init script.
- The cron job which implements persistence on other Linuxes and runs at reboot is not created on OpenWrt.
- geoip-shell integrates into firewall3 or firewall4 via what's called a "firewall include". On OpenWrt, a firewall include is a setting which tells firewall3 or firewall4 to do something specific in response to certain events.
- The only task of the init script for geoip-shell is to call the geoip-shell-mk-fw-include.sh script, which makes sure that the firewall include exists and is correct, if not then creates the include.
- The firewall include is what does the actual persistence work. geoip-shell firewall include triggers on firewall reload (which happens either at reboot or when the system decides that a reload of the firewall is necessary, or when initiated by the user).
- When triggered, the include script calls the -run script with the "restore" action.
- The -run script verifies that geoip nftables/iptables rules and ip sets exist, and if not then it restores them from backup, or (if backup doesn't exist) initiates re-fetch of the ip lists and then re-creates the rules and the ip sets.
- By default, geoip-shell does not create backups on OpenWrt because typically the permanent storage is very small and prone to wear.
- Automatic updates of ip lists on OpenWrt are triggered from a cron job like on other Linuxes.

## Defaults for OpenWrt
Generally the defaults are the same as for other systems, except:
- the data directory which geoip-shell uses to store the status file and the backups is by default in `/tmp/geoip-shell-data`, rather than in `/var/lib/geoip-shell` as on other Linux systems. This is to avoid flash wear. You can change this by running the install script with the `-a <path>` option, or after installation via the command `geoip-shell configure -a <path>`.
- the 'nobackup' option is set to 'true', which configures geoip-shell to not create backups of the ip lists. With this option, geoip-shell will work as usual, except after reboot (and for iptables-based systems, after firewall restart) it will re-fetch the ip lists, rather than loading them from backup. You can change this by running the -install script with the `-o false` option (`-o` stands for nobackup), or after installation via the command `geoip-shell configure -o false`. To have persistent ip list backups, you will also need to change the data directory path as explained above.
- if using geoip-shell on a router with just a few MB of embedded flash storage, consider either leaving the nobackup and datadir path defaults as is, or connecting an external storage device to your router (preferably formatted to ext4) and configuring a directory on it as your geoip-shell data directory, then enabling automatic backups. For example, if your external storage device is mounted on _/mnt/somedevice_, you can do all this via this command: `geoip-shell configure -a /mnt/somedevice/geoip-shell-data -o false`.
- the default ip lists source for OpenWrt is ipdeny (rather than ripe). While ipdeny is a 3rd party, they provide aggregated lists which consume less memory (on nftables-based systems the ip lists are automatically optimized after loading into memory, so there the source does not matter, but a smaller initial ip lists size will cause a smaller memory consumption spike while loading the ip list).

This is about it for this document. Much more information is available in the main [README.md](README.md) and in the extra _.md_ files inside the Documentation directory. If you have any questions, contact me in this thread:
https://forum.openwrt.org/t/geoip-shell-flexible-geoip-blocker-for-linux-now-supports-openwrt/189611

If you use this project, I will be happy to hear about your experience in the above thread. If for some reason geoip-shell is not working for you, I will want to know that as well so I can improve it.

---

### Overview (`net/geoip-shell/README.md`)

# **geoip-shell**
Geoip blocker for Linux. Supports both **nftables** and **iptables** firewall management utilities.

The idea of this project is making geoip blocking easy on (almost) any Linux system, no matter which hardware, including desktop, server, VPS or router, while also being reliable and providing flexible configuration options for the advanced users.

Supports running on OpenWrt. Supports ipv4 and ipv6.

## Table of contents
- [Main Features](#main-features)
- [Usage](#usage)
- [Pre-requisites](#pre-requisites)
- [Notes](#notes)
- [In detail](#in-detail)
- [OpenWrt](#openwrt)
- [Privacy](#privacy)

## **Main Features**
* Core functionality is creating either a whitelist or a blacklist in the firewall using automatically downloaded ip lists for user-specified countries.

* ip lists are fetched either from **RIPE** (regional Internet registry for Europe, the Middle East and parts of Central Asia) or from **ipdeny**. Both sources provide updated ip lists for all regions.

* All firewall rules and ip sets required for geoip blocking to work are created automatically during installation or setup.

* Implements optional (enabled by default) persistence of geoip blocking across system reboots and automatic updates of the ip lists.

* After installation, a utility is provided to check geoip status and firewall rules or change country codes and geoip-related config.

### **Reliability**:
- Downloaded ip lists go through validation which safeguards against application of corrupted or incomplete lists to the firewall.

<details> <summary>Read more:</summary>

- With nftables, utilizes nftables atomic rules replacement to make the interaction with the system firewall fault-tolerant and to completely eliminate time when geoip is disabled during an automatic update.
- All scripts perform extensive error detection and handling.
- All user input is validated to reduce the chance of accidental mistakes.
- Verifies firewall rules coherence after each action.
- Automatic backup of geoip-shell state (optional, enabled by default except on OpenWrt).
- Automatic recovery of geoip-shell firewall rules after a reboot (a.k.a persistence) or in case of unexpected errors.
- Supports specifying trusted ip addresses anywhere on the Internet which will bypass geoip blocking to make it easier to regain access to the machine if something goes wrong.
</details>

### **Efficiency**:
- Utilizes the native nftables sets (or, with iptables, the ipset utility) which allows to create efficient firewall rules with thousands of ip ranges.

<details><summary>Read more:</summary>

- With nftables, optimizes geoip blocking for low memory consumption or for performance, depending on the RAM capacity of the machine and on user preference. With iptables, automatic optimization is implemented.
- Ip list parsing and validation are implemented through efficient regex processing which is very quick even on slow embedded CPU's.
- Implements smart update of ip lists via data timestamp checks, which avoids unnecessary downloads and reconfiguration of the firewall.
- Uses the "prerouting" hook in kernel's netfilter component which shortens the path unwanted packets travel in the system and may reduce the CPU load if any additional firewall rules process incoming traffic down the line.
- Supports the 'ipdeny' source which provides aggregated ip lists (useful for embedded devices with limited memory).
- Scripts are only active for a short time when invoked either directly by the user or by the init script/reboot cron job/update cron job.

</details>

### **User-friendliness**:
- Good command line interface and useful console messages.

<details><summary>Read more:</summary>

- Extensive and (usually) up-to-date documentation.
- Sane settings are applied during installation by default, but also lots of command-line options for advanced users or for special corner cases are provided.
- Provides a utility (symlinked to _'geoip-shell'_) for the user to change geoip config (turn geoip on or off, change country codes, change geoip blocking mode, change ip lists source, change the cron schedule etc).
- Provides a command _('geoip-shell status')_ to check geoip blocking status, which also reports if there are any issues.
- In case of an error or invalid user input, provides useful error messages to help with troubleshooting.
- All main scripts display detailed 'usage' info when executed with the '-h' option.
- The code should be fairly easy to read and includes a healthy amount of comments.
</details>

### **Compatibility**:
- Since the project is written in POSIX-compliant shell code, it is compatible with virtually every Linux system (as long as it has the [pre-requisites](#pre-requisites)). It even works well on simple embedded routers with 8MB of flash storage and 128MB of memory (for nftables, 256MB is recommended if using large ip lists such as the one for US until the nftables team releases a fix reducing memory consumption).

<details><summary>Read more:</summary>

- Supports running on OpenWrt.
- The project avoids using non-common utilities by implementing their functionality in custom shell code, which makes it faster and compatible with a wider range of systems.
</details>

## **Usage**

If you want to change geoip blocking config or check geoip blocking status, you can do that via the provided utilities.
A selection of options is given here, for additional options run `geoip-shell -h` or read [NOTES.md](NOTES.md)and [DETAILS.md](DETAILS.md).

**To check current geoip blocking status:** `geoip-shell status`. For a list of all firewall rules in the geoip chain and for a detailed count of ip ranges in each ip list: `geoip-shell status -v`.

**To add or remove ip lists for countries:** `geoip-shell <add|remove> -c <"country_codes">`

_<details><summary>Examples:</summary>_
- example (to add ip lists for Germany and Netherlands): `geoip-shell add -c "DE NL"`
- example (to remove the ip list for Germany): `geoip-shell remove -c DE`
</details>

**To enable or disable geoip blocking:** `geoip-shell <on|off>`

**To change ip lists source:** `geoip-shell configure -u <ripe|ipdeny>`

**To change geoip blocking mode:** `geoip-shell configure -m <whitelist|blacklist>`

**To have certain trusted ip addresses or subnets bypass geoip blocking:** `geoip-shell configure -t <["ip_addresses"]|none>`. `none` removes previously set trusted ip addresses.

**To have certain LAN ip addresses or subnets bypass geoip blocking:** `geoip-shell configure -l <["ip_addresses"]|auto|none>`. `auto` will automatically detect LAN subnets (only use this if the machine has no dedicated WAN interfaces). `none` removes previously set LAN ip addresses. This is only needed when using geoip-shell in whitelist mode, and typically only if the machine has no dedicated WAN network interfaces. Otherwise you should apply geoip blocking only to those WAN interfaces, so traffic from your LAN to the machine will bypass the geoip filter.

**To change protocols and ports geoblocking applies to:** `geoip-shell configure -p <[tcp|udp]:[allow|block]:[all|<ports>]>`

_(for detailed description of this feature, read [NOTES.md](NOTES.md), sections 9-11)_

**To enable or change the automatic update schedule:** `geoip-shell configure -s <"schedule_expression">`

_<details><summary>Example</summary>_

`geoip-shell configure -s "1 4 * * *"`

</details>

**To disable automatic updates of ip lists:** `geoip-shell configure -s disable`

**To update or re-install geoip-shell:** run the -install script from the (updated) distribution directory. It will first run the -uninstall script of the older/existing version, then install the new version.

On OpenWrt, if installed via an ipk package: `opkg uninstall <geoip-shell|geoip-shell-iptables>`

## **Pre-requisites**
- **Linux**. Tested on Debian-like systems and on OpenWrt, should work on any desktop/server distribution and possibly on some other embedded distributions.
- **POSIX-compliant shell**. Works on most relatively modern shells, including **bash**, **dash**, **ksh93**, **yash** and **ash** (including Busybox **ash**). Likely works on **mksh** and **lksh**. Other flavors of **ksh** may or may not work _(please let me know if you try them)_. Does **not** work on **tcsh** and **zsh**.

- **nftables** - firewall management utility. Supports nftables 1.0.2 and higher (may work with earlier versions but I do not test with them).
- OR **iptables** - firewall management utility. Should work with any relatively modern version.
- for **iptables**, requires the **ipset** utility - install it using your distribution's package manager
- standard Unix utilities including **tr**, **cut**, **sort**, **wc**, **awk**, **sed**, **grep**, **pgrep**, **pidof** and **logger** which are included with every server/desktop linux distribution (and with OpenWrt). Both GNU and non-GNU versions are supported, including BusyBox implementation.
- **wget** or **curl** or **uclient-fetch** (OpenWrt-specific utility).
- for the autoupdate functionality, requires the **cron** service to be enabled.

## **Notes**
For some helpful notes about using this suite, read [NOTES.md](NOTES.md).

## **In detail**
For specifics about each script, read [DETAILS.md](DETAILS.md).

## **OpenWrt**
For information about OpenWrt support, read the [OpenWrt README](OpenWrt-README.md).

## **Privacy**
geoip-shell does not share your data with anyone.
If you are using the ipdeny source then note that they are a 3rd party which has its own data privacy policy.

---

### Overview (`net/https-dns-proxy/README.md`)

# https-dns-proxy

[](https://openwrt.org)
[](https://docs.mossdef.org/https-dns-proxy/)
[](https://docs.mossdef.org/https-dns-proxy/)
[](https://github.com/stangri/https-dns-proxy)
[](https://github.com/stangri/https-dns-proxy/blob/master/LICENSE)

A lightweight, RFC8484-compliant DNS-over-HTTPS (DoH) proxy service for OpenWrt.  
Includes optional integration with `dnsmasq`, automatic fallback, and canary domain support.

## Features

- Small footprint (~40KB installed)
- Seamless dnsmasq integration and fallback
- Optional LuCI Web UI with 40+ built-in resolvers

**Full documentation:**

[https://docs.mossdef.org/https-dns-proxy/](https://docs.mossdef.org/https-dns-proxy/)

Based on [@aarond10](https://github.com/aarond10)'s excellent [https_dns_proxy](https://github.com/aarond10/https_dns_proxy)

---

### Overview (`net/modemmanager/README.md`)

# OpenWrt ModemManager

## Description

Cellular modem control and connectivity

Optional libraries libmbim and libqmi are available.
Your modem may require additional kernel modules and/or the usb-modeswitch
package.

## Usage

Once installed, you can configure the 2G/3G/4G modem connections directly in
/etc/config/network as in the following example:

    config interface 'broadband'
        option device      '/sys/devices/platform/soc/20980000.usb/usb1/1-1/1-1.2/1-1.2.1'
        option proto       'modemmanager'
        option apn         'ac.vodafone.es'
        option allowedauth 'pap chap'
        option username    'vodafone'
        option password    'vodafone'
        option pincode     '7423'
        option iptype      'ipv4'
        option plmn        '214001'
        option lowpower    '1'
        option signalrate  '30'
        option allow_roaming '1'
        option force_connection '1'
        option init_epsbearer '<modem|network|connection|custom>'
        option timeout     '120'

Only 'device' and 'proto' are mandatory options, the remaining ones are all
optional.

The 'allowedauth' option allows limiting the list of authentication protocols.
It is given as a space-separated list of values, including any of the
following: 'pap', 'chap', 'mschap', 'mschapv2' or 'eap'. It will default to
allowing all protocols.

The 'iptype' option supports any of these values: 'ipv4', 'ipv6' or 'ipv4v6'.
It will default to 'ipv4' if not given.

The 'plmn' option allows to set the network operator MCCMNC.

The 'signalrate' option set's the signal refresh rate (in seconds) for the device.
You can call signal info with command: mmcli -m 0 --signal-get

The 'timeout' option set's the command timeout (in seconds) for the long
running 'mmcli' calls ('enable', 'simple-connect',
3gpp-register-in-operator' and '3gpp-set-initial-eps-bearer-settings')
in the modemmanager protohandler. The default value is 120 seconds, if
no value is configured.

The 'force_connection' option is designed to ensure that the modem automatically
attempts to reconnect regardless of any errors encountered during the
connection process.

On 4G and 5G networks, the modem attaches to the network with an initial
EPS bearer, before the data connection is established. The 'init_epsbearer'
option selects where the settings for this initial EPS bearer come from:
* modem:      Leave the initial EPS bearer settings stored on the modem
              unchanged (default). These are usually provided by the carrier
              firmware or set by a previous configuration.
* network:    Set an empty initial EPS bearer, so the network assigns the APN.
              Use this to override settings left on the modem, e.g. after
              swapping the SIM card.
* connection: Use the options 'apn', 'iptype', 'allowedauth', 'username' and
              'password', the same options as for the data connection.
* custom:     Use separate options, prefixed with 'init_': 'init_apn',
              'init_iptype', 'init_allowedauth', 'init_username' and
              'init_password'.

The values 'none' and 'default' are deprecated aliases for 'modem' and
'connection'.

The initial EPS bearer settings stored on the modem can also be cleared once
manually:

    mmcli -m <modem> --3gpp-set-initial-eps-bearer-settings=""

---

### Overview (`net/netifyd/README.md`)

# Netify Agent
Copyright ©2015-2020 eGloo Incorporated ([www.egloo.ca](https://www.egloo.ca))

## Network Intelligence - Simplified
The [Netify Agent](https://www.netify.ai/) is a deep-packet inspection server.  The Agent is built on top of [nDPI](http://www.ntop.org/products/deep-packet-inspection/ndpi/) (formerly OpenDPI) to detect network protocols and applications.  Detections can be saved locally, served over a UNIX or TCP socket, and/or "pushed" (via HTTP POSTs) to a remote third-party server.  Flow metadata, network statistics, and detection classifications are stored using JSON encoding.

Optionally, the Netify Agent can be coupled with a [Netify Cloud](https://www.netify.ai/) subscription for further cloud processing, historical storage, machine-learning analysis, event notifications, device detection/identification, along with the option (on supported platforms) to take an active role in policing/bandwidth-shaping specific network protocols and applications.

## Download Packages
Supported platforms with installation instructions can be found [here](https://www.netify.ai/get-netify).

Alternatively, binary packages are available for the following OS distributions (manual install):
- [CentOS](http://download.netify.ai/netify/centos/)
- [ClearOS](http://download.netify.ai/netify/clearos/)
- [Debian](http://download.netify.ai/netify/debian/)
- [FreeBSD](http://download.netify.ai/netify/freebsd/)
- [NethServer](http://download.netify.ai/netify/nethserver/)
- [OpenWrt/LEDE](https://downloads.openwrt.org/snapshots/packages/)
- [pfSense](http://download.netify.ai/netify/pfsense/)
- [Raspbian](https://software.opensuse.org//download.html?project=home%3Aegloo&package=netifyd)
- [RHEL](http://download.netify.ai/netify/rhel/)
- [Ubuntu](http://download.netify.ai/netify/ubuntu/)

### Runtime Requirements
- [Linux] Ensure that the nfnetlink and nf_conntrack_netlink kernel modules are loaded if NAT detection is enabled.

## Download Source
When cloning the source tree, ensure you use `--recursive` to include all
sub-modules.

### Build Requirements
Netify requires the following third-party packages:
- libcurl
- libpcap
- zlib
- [Linux] libmnl
- [Linux] libnetfilter-conntrack

Optional:
- google-perftools/gperftools/libtcmalloc (will use bundled version when not available)

### Configuring/Building From Source
Read the appropriate documentation in the doc directory, prefixed with: `BUILD-*`

Generally the process is:
```sh
./autogen.sh
./configure
make
```

## Online Documentation
Further user and developer documentation can be found [here](https://www.netify.ai/resources).

## License
This software is licenced under the [GPLv3](https://www.gnu.org/licenses/gpl-3.0.txt):
>>>
Copyright (C) 2015-2020 eGloo Incorporated

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
>>>

---

### Overview (`net/ntpd/README.md`)

# Configuring NTPD with UCI

## Precedent
Sysntpd is the lightweight implementation of the NTP protocol under
Busybox.  It supports many (but not all) of the same parameters.

It is configured as a `config timeserver ntp` section in `/etc/config/system`,
below.

## Configuration

A sample configuration looks like:

**/etc/config/system**:

```
config timeserver ntp
	option enabled 1
	option enable_server 1
	list server tick.udel.edu
	list server tock.udel.edu
	list interface eth0
	list interface eth1
	list interface eth2
```

If you want to temporarily disable the service without deleting all of the
configuration state, this is done by clearing the `enabled` parameter.  If
this parameter is `1` (the default), the service is enabled.

The service can run as a stand-alone client (`enable_server 0`, the default)
or it can also operate as a server in turn to local clients, by setting this
parameter to `1`.

The parameter(s) `server` enumerate a list of servers to be used for
reference NTP servers by the local daemon.  At least one is required,
and two or more are recommended (unless you have an extremely available
local server).  They should be picked to be geographically divergent,
and preferably reachable via different network carriers to protect
against network partitions, etc.  They should also be high-quality
time providers (i.e. having stable, accurate clock sources).

The `interface` parameter enumerates the list of interfaces on which
the server is reachable (see `enable_server 1` above), and may be a
subset of all of the interfaces present on the system.  For security
reasons, you may elect to only offer the service on internal networks.
If omitted, it defaults to _all_ interfaces.

## Differences with `sysntpd`

Busybox `sysntpd` supports configuring servers based on DHCP
provisioning (option 6, per the [DHCP and BOOTP
Parameter](https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml)
list from IANA).  This functionality is enabled (in Busybox) with the
`use_dhcp` boolean parameter (default `1`), and the `dhcp_interface`
list parameter, which enumerates the interfaces whose provisioning
is to be utilized.

### Considerations for DHCP-provisioned NTP servers

Most terrestrial and satellite ISPs have access to very high-quality
clock sources (these are required to maintain synchronization on T3,
OC3, etc trunks or earth terminals) but seldom offer access to those
time sources via NTP in turn to their clients, mostly from a misplaced
fear that their time source might come under attack (a slave closely
tied to the master could also provide extremely high-quality time
without the risk of network desynchronization should it come under
sophisticated attack).

As a result, the NTP servers that your ISP may point you at are
often of unknown/unverified quality, and you use them at your own
risk.

Early millennial versions of Windows (2000, XP, etc) used NTP only
to _initially set_ the clock to approximately 100ms accuracy (and
not maintain synchronization), so the bar wasn't set very high.
Since then, requirements for higher-quality timekeeping have
arisen (e.g. multi-master SQL database replication), but most ISPs
have not kept up with the needs of their users.

Current releases of Windows use Domain Controllers for time
acquisition via the [NT5DS protocol](https://blogs.msdn.microsoft.com/w32time/2007/07/07/what-is-windows-time-service/)
when domain joined.

Because of the unreliable quality of NTP servers DHCP-provisioned by
ISPs, support for this functionality was deemed unnecessary.

---

### Overview (`net/openthread-br/README.md`)

# OpenThread Border Router

This package contains the OpenThread Border Router.

## Requirements

To use this package, you need a Thread Radio Co-Processor (RCP). Testing of
this package was done with Nordic Semiconductor nRF52840 USB dongles.

Building and flashing the dongle with the Thread RCP firmware is out of scope
of this document.

One caveat for this dongle is worth mentioning here. The nRF52840 USB dongle
seems to come with the U2F bootloader. To get it in mass storage mode to copy a
firmware file, you need to plug it in while pressing the reset button. However,
after the initial flash with the ot-rcp firmware, this method stops working.
Instead, you need to double press the reset button after plugging in the dongle.

## Packaging decisions

### Configurable package build

OpenThread is complex software. Adding config options to change the build of
the package will likely result in more bug reports. As the package and its
dependencies are unlikely to fit in any router with small flash (16MB or less),
I don't see much point in making things configurable for reducing size either.

### Firewall support

OpenWrt uses firewall4 with nftables by default, but the OpenThread firewall
implementation uses IPTables and IPset. While we still support firewall3 with
IPTables, it's not a good idea to add new dependencies to old things.
Therefore, firewall support is disabled completely.

This can be revised once the following feature request is implemented:
https://github.com/openthread/ot-br-posix/issues/1675

### mDNSResponder

The package depends on mDNSResponder. The alternative, Avahi, depends on D-Bus,
which is not something I feel comfortable with running on any router. While
there are Avahi packages without D-Bus support, using OpenThread Border Router
with Avahi requires libavahi-client, and this requires Avahi to be built with
D-Bus support.

### REST Server

The REST server is enabled to make this package compatible with Home Assistant.

### TREL support

Thread Radio Encapsulation Link support is enabled, as it allows Border Routers
to communicate over other links (e.g. Ethernet), reducing traffic over the
802.15.4 radios.

The following Github discussion contains a good explanation of TREL:
https://github.com/openthread/openthread/discussions/8478

### UCI/netifd support

The package contains a minimal netifd protocol handler. This allows configuring
the Thread network in /etc/config/network. The agent will be started by netifd,
rather than using an init script.

OpenThread does not store prefix information in non-volatile storage. As a
result, every time the agent is restarted, a different prefix would be used.
This is not very nice, and makes it very difficult to run the OpenThread Border
Router on a device that is not your main router. Therefore, prefixes can be
configured in /etc/config/network. This way, you can add a static route to the
Thread prefix(es) in your main router, making it possible to access devices on
the Thread network from your entire network.

## Create network

When starting the OpenThread Border Router for the first time, a Thread network
must be created.

As the agent is started by netifd, we first need to create an interface in
/etc/config/network:

```
config interface 'thread'
        option device 'wpan0'
        option proto 'openthread'
        option backbone_network 'lan'
        option radio_url 'spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=460800'
        list prefix 'fd6f:5772:5468:7200::/64 paros'
        option verbose '0'
```

Prefix and verbose are optional. Everything else is required. The protocol
handler will fail if a required setting is missing. If something isn't working,
check ifstatus for the OpenThread interface:

```
# ifup thread
# ifstatus thread
{
        "up": false,
        "pending": false,
        "available": true,
        "autostart": false,
        "dynamic": false,
        "proto": "openthread",
        "data": {

        },
        "errors": [
                {
                        "subsystem": "openthread",
                        "code": "MISSING_BACKBONE_NETWORK"
                }
        ]
}
```

In the above example, the backbone_network option is missing.

The protocol handler will automatically start the the Thread network, so we
need to bring it down for the initial setup. This only needs to be done once.

```
ubus call otbr threadstop
```

### LuCI

Creating a network in LuCI appears to be broken for the moment.

### CLI

```
ot-ctl dataset init new
ot-ctl dataset panid 0x12ab
ot-ctl dataset extpanid 12ab12ab12ab12ab
ot-ctl dataset networkname OpenWrThread
ot-ctl dataset networkkey ddf429af1c52d1735ffaf36fae343ee8
ot-ctl dataset commit active
ot-ctl ifconfig up
ot-ctl thread start
ot-ctl netdata register
```

### Configure route

Before you can join a device to your new Thread network, you must add a route
to the Thread prefix on the commissioner device via the OpenWrt router running
the OpenThread Border Router.

Get the prefix:
```
ot-ctl prefix
```

Example output:

```
fd6b:a92f:c531:1::/64 paros low f000
Done
```

Configuring the route is out of scope of this document, but it must be done, or
joining Thread devices will fail.

### Get hex-encoded operational dataset TLV

This is needed to join devices to the Thread Network.

```
ot-ctl dataset active -x
```

Example output:

```
0e080000000000010000000300001035060004001fffe00708fd488c6a892ec30c04106e220c964a14a7e10e9004691920ec390c0402a0f7f80102ffff030b5468726541646c6576696f0208ffffffffffffffff0510ddf429af1c52d1735ffaf36fae343ee8
```

## Join another OpenThread Border Router

Simply configure the active dataset in /etc/config/network:

```
config interface 'thread'
        option device 'wpan0'
        option proto 'openthread'
        option backbone_network 'lan'
        option dataset '0e080000000000010000000300000f35060004001fffe0020836b86cd9746ab3080708fd9850cbe719b1d205101f11a11320828c7a6ebc2f2e675c0dca030e686f6d652d617373697374616e740102716f041025804ed78614258ebedf4e2db37b3b6e0c0402a0f7f8'
        list prefix 'fd6f:5772:5468:7200::/64 paros'
        option radio_url 'spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=460800'
        option verbose '0'
```

Afterwards, bring up the interface:

```
ifup thread
```

## Join a Thread device via Matter

### ESP32
The following procedure has been tested with an ESP32-C6 using [the Matter
lighting-app example](https://github.com/project-chip/connectedhomeip/tree/master/examples/lighting-app/esp32).
Building and flashing that app is out of scope of this document.

During startup, the lighting app will print the SetupQRCode to the serial
console:

```
I (1614) chip[SVR]: SetupQRCode: [MT:Y.K9042C00KA0648G00]
I (1624) chip[SVR]: Copy/paste the below URL in a browser to see the QR Code:
I (1634) chip[SVR]: https://project-chip.github.io/connectedhomeip/qrcode.html?data=MT%3AY.K9042C00KA0648G00
I (1644) chip[SVR]: Manual pairing code: [34970112332]
```

Decide on a node ID for the device.

```
./chip-tool pairing code-thread 0x65737933320000 hex:0e080000000000010000000300001035060004001fffe00708fd488c6a892ec30c04106e220c964a14a7e10e9004691920ec390c0402a0f7f80102ffff030b5468726541646c6576696f0208ffffffffffffffff0510ddf429af1c52d1735ffaf36fae343ee8 MT:Y.K9042C00KA0648G00 --paa-trust-store-path /path/to/connectedhomeip/credentials/test/attestation/
```

---

### Overview (`net/openvswitch/README.md`)

# Which packages to install

Install `openvswitch` if you need OpenFlow virtual switch function.  It
contains ovs-vswitchd, ovsdb-server and helper utilities such as ovs-vsctl,
ovs-ofctl, ovs-ctl etc.

Linux kernel datapath module openvswitch.ko will also be installed along with
package `openvswitch`.  Tunnel encap support for gre, geneve, vxlan can be
included by installing `kmod-openvswitch-{gre,geneve,vxlan}` respectively

For OVN deployment

- Install `openvswitch-ovn-north` for ovs-northd, ovsdb-server, ovn helper utitlies
- Install `openvswitch-ovn-host` for ovn-controller and `openvswitch`

# How to use them

Open vSwitch provides a few very useful helper script in
`/usr/share/openvswitch/scripts/`.  A simple initscript is provided.  It's
mainly a wrapper around `ovs-ctl` and `ovn-ctl` with simple knobs from
`/etc/config/openvswitch`.  Procd is not used here.

	/etc/init.d/openvswitch start
	/etc/init.d/openvswitch stop
	/etc/init.d/openvswitch stop north
	/etc/init.d/openvswitch restart ovs
	/etc/init.d/openvswitch status

Use `ovs-ctl` and `ovn-ctl` directly for more functionalities

# Open vSwitch in-tree Linux datapath modules

The Open vSwitch build system uses regexp and conditional-compilation
heuristics to support building the shipped kernel module source code against a
wide range of kernels, as of openvswitch-2.10, the list is supposed to include
vanilla linux 3.10 to 4.15, plus a few distro kernels.

It may NOT work

 - Sometimes the code does not compile
 - Sometimes the code compiles but insmod will fail
 - Sometimes modules are loaded okay but actually does not function right

For these reasons, the in-tree datapath modules are NOT visible/enabled by
default.

Building and using in-tree datapath modules requires some level of devel
abilities to proceed.  You are expected to configure build options and build
the code on your own

E.g. pair openvswitch userspace with in-tree datapath module

	CONFIG_DEVEL=y
	CONFIG_PACKAGE_openvswitch=y
	# CONFIG_PACKAGE_kmod-openvswitch is not set
	CONFIG_PACKAGE_kmod-openvswitch-intree=y

E.g. replace in-tree datapath module with upstream version

	opkg remove --force-depends kmod-openvswitch-intree
	opkg install kmod-openvswitch
	ovs-ctl force-reload-kmod

# UCI configuration options

There are 5 config section types in package openvswitch:
ovs ovn_northd, ovn_controller & ovs_bridge.

Each of these supports a disabled option, which should be 
set to 0 to launch the respective daemons.

The ovs section section also supports the options below, to configure a set of
SSL CA, certificate and private key. After adding these to Open vSwitch, you
may specify ssl: connection methods for e.g. the OpenFlow controller. Note that
Open vSwitch only reads these files during startup, so it needs to be restarted
after adding or changing these options.

| Name     | Type    | Required | Default | Description                       |
|----------|---------|----------|---------|-----------------------------------|
| disabled | boolean | no       | 0       | If set to 1, do not configure SSL |
| ca       | string  | no       | (none)  | Path to CA certificate            |
| cert     | string  | no       | (none)  | Path to certificate               |
| key      | string  | no       | (none)  | Path to private key               |

The ovs_bridge section also supports the options below,
for initialising a virtual bridge with an OpenFlow controller.

| Name               | Type    | Required | Default                        | Description                                                |
|--------------------|---------|----------|--------------------------------|------------------------------------------------------------|
| disabled           | boolean | no       | 0                              | If set to true, disable initialisation of the named bridge |
| name               | string  | no       | Inherits UCI config block name | The name of the switch in the OVS daemon                   |
| controller         | string  | no       | (none)                         | The endpoint of an OpenFlow controller for this bridge     |
| datapath_id        | string  | no       | (none)                         | The OpenFlow datapath ID for this bridge                   |
| datapath_desc      | string  | no       | (none)                         | The OpenFlow datapath description for this bridge          |
| drop_unknown_ports | boolean | no       | 0                              | Remove ports not defined in UCI from the bridge            |
| fail_mode          | string  | no       | standalone                     | The bridge failure mode                                    |
| ports              | list    | no       | (none)                         | List of ports to add to the bridge                         |

The ovs_port section can be used to add ports to a bridge. It supports the options below.

| Name     | Type    | Required | Default | Description
| ---------|---------|----------|---------|------------------------------------------------|
| disabled | boolean | no       | 0       | If set to 1, do not add the port to the bridge |
| bridge   | string  | yes      | (none)  | Name of the bridge to add the port to          |
| port     | string  | yes      | (none)  | Name of the port to add to the bridge          |
| ofport   | integer | no       | (none)  | OpenFlow port number to be used by the port    |
| tag      | integer | no       | (none)  | 802.1Q VLAN tag to set on the port             |
| type     | string  | no       | (none)  | Port type, e.g. internal, erspan, type, ...    |

---

### Overview (`net/pbr/README.md`)

# Policy-Based Routing (pbr)

[](https://openwrt.org)
[](https://docs.openwrt.melmac.ca/pbr/)
[](https://github.com/stangri/pbr/blob/master/LICENSE)

Flexible policy-based routing (PBR) framework for OpenWrt.  
Allows routing specific traffic (by IP, MAC, port, protocol, or domain) through a specific WAN, VPN, or tunnel.

## Features

- Route by IP, MAC, port, or domain name
- Works with WAN, VPNs (WireGuard, OpenVPN), or tunnels
- Lightweight shell-based implementation
- Optional Web UI integration via LuCI

**Full documentation:**  
[https://docs.openwrt.melmac.ca/pbr/](https://docs.openwrt.melmac.ca/pbr/)

---

### Overview (`net/pppossh/README.md`)

This package will add the so-called `pppossh` protocol support to OpenWrt.  The idea is mainly from [`pvpn` project](https://github.com/halhen/pvpn) (poor man's VPN over SSH).

PPPoSSH is generally not considered a network setup for production use mainly due to the TCP-over-TCP styles of traffic transport, but it can be quite handy for personal use.  And with what's already in OpenWrt, it is really easy and takes little extra space to configure it up and running.

## Prerequisites and dependency.

`pppossh` depends on either `dropbear` or `openssh-client`; `dropbear` is normally enabled in OpenWrt by default.

The following requirements need to be fulfilled for it to work.

- A SSH account on the remote machine with `CAP_NET_ADMIN` capability is required.
- Public key authentication must be enabled and setup properly.

	Public key of the one generated automatially by dropbear can be induced by the following command.  But you can always use your own (dropbear can work with OpenSSH public key).

		dropbearkey -y -f /etc/dropbear/dropbear_rsa_host_key

- SSH server's fingerprint has to be present in `~/.ssh/known_hosts` for the authentication to proceed in an unattended way.

	Manually logging in at least once to the remote server from OpenWrt should do this for you.

## How to use it.

The protocol name to use in `/etc/config/network` is `pppossh`.  Options are as described below.

- `server`, SSH server name
- `port`, SSH server port (defaults to `22`).
- `sshuser`, SSH login username
- `identity`, list of client private key files.  `~/.ssh/id_{rsa,dsa}` will
   be used if no identity file was specified and at least one of them must be
   valid for the public key authentication to proceed.
- `ipaddr`, local ip address to be assigned.
- `peeraddr`, peer ip address to be assigned.
- `ssh_options`, extra options for the ssh client.
- `peer_pppd_options`, extra options for the pppd command run on the peer side.
- `use_hostdep`, set it to `0` to disable the use of `proto_add_host_dependency`.  This is mainly for the case that the appropriate route to `server` is not registered to `netifd` and thus causing a incorrect route being setup.

## Tips

An `uci batch` command template for your reference.  Modify it to suite your situation.

	uci batch <<EOF
	delete network.fs
	set network.fs=interface
	set network.fs.proto=pppossh
	set network.fs.sshuser=root
	set network.fs.server=ssh.example.cn
	set network.fs.port=30244
	add_list network.fs.identity=/etc/dropbear/dropbear_rsa_host_key
	set network.fs.ipaddr=192.168.177.2
	set network.fs.peeraddr=192.168.177.1
	commit
	EOF

Allow forward and NAT on the remote side (`ppp0` is the peer interface on the remote side.  `eth0` is the interface for Internet access).

	sysctl -w net.ipv4.ip_forward=1
	iptables -t filter -A FORWARD -i ppp0 -j ACCEPT
	iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

It's possible that pppd may output protocol negotiation incompatibilities issues to syslog, such as something like the following which did not hurt the connectivity and was annoying only because we thought it can do better.

	Sun Oct 25 09:45:14 2015 daemon.err pppd[22188]: Received bad configure-rej:  12 06 00 00 00 00

To debug such problems, we can try adding `option pppd_optinos debug` to the interface config.  In the above case, it's a LCP CCP configure rej (the CCP options struct is exactly 6 octets in size as indicated in source code `pppd/ccp.h`) and since the internet fee is not charged on the bytes transferred, I will just use `noccp` to disable the negotiation altogether.

Also to optimize bulk transfer performance, you can try tweaking the ciphers.  OpenSSH client does not support `none` cipher by default and you have to patch and install it by yourself.  Another option is to try ciphers like `arcfour` and `blowfish-cbc`.  In my case, `arcfour` has the best throughput.

	option ssh_options '-o "Ciphers arcfour"'

---

### Overview (`net/safe-search/Readme.md`)

# Package: net/safe-search

This package prevents adult content from appearing in search results by
configuring dnsmasq to force all devices on your network to use Google and
Bing's Safe Search IP addresses. This is designed to be appropriate for most
businesses and families. The default filtering rules do not interfere with
normal web browsing.

Enabled and disable Safe Search by editing /etc/config/safe-search . To make
your changes active, run safe-search-update.

Currently Supported:
- Enabled By Default
    - www.bing.com Safe Search
        -  https://help.bing.microsoft.com/#apex/18/en-US/10003/0
    - DuckDuckGo.com Safe Search
        - https://duck.co/help/features/safe-search
    - www.Google.com Safe Search
        - https://support.google.com/websearch/answer/186669
- Not Enabled By Default:
    - youtube Safe Search
        - https://support.google.com/a/answer/6214622
        - https://support.google.com/a/answer/6212415
        - https://www.youtube.com/check_content_restrictions
        - Not enabled because it is designed for children, and may annoy adults...

---

### Overview (`net/shunt/README.md`)

# shunt - policy based routing by mac, source, destination and domain

## Table of Contents
* [Description](#description)
* [Quick Start](#quick-start)
* [Main Features](#main-features)
* [Prerequisites](#prerequisites)
  * [Which tunnels work](#which-tunnels-work)
* [Installation and Usage](#installation-and-usage)
* [shunt CLI interface](#shunt-cli-interface)
* [shunt config options](#shunt-config-options)
* [How addresses are learned](#how-addresses-are-learned)
  * [What polling costs](#what-polling-costs)
* [Examples](#examples)
* [What it shells out to](#what-it-shells-out-to)
* [What shunt creates on the system](#what-shunt-creates-on-the-system)
* [Coexistence with pbr and mwan3](#coexistence-with-pbr-and-mwan3)
* [Troubleshooting & debug options](#troubleshooting-and-debug-options)
* [Known limitations](#known-limitations)
* [Support](#support)
* [Removal](#removal)
* [Donations](#donations)

<a id="description"></a>
## Description
shunt routes selected traffic into a policy interface - a VPN tunnel, a second uplink, a mobile connection - chosen by client address, client MAC, destination or domain. It keeps its own nftables table and one routing table per policy, so it coexists with fw4 and with other routing tools instead of competing with them.

The one thing that defines the project: **shunt is not bound to any DNS backend.** It works unchanged with dnsmasq, unbound, smartdns, AdGuard Home or anything else, because it never asks the resolver for anything and never sits in the DNS path. Domain to address mapping comes from two sources shunt owns itself, described under [How addresses are learned](#how-addresses-are-learned).

<a id="quick-start"></a>
## Quick Start
For a typical setup these few steps are enough - see the sections below for details:
1. Install the LuCI companion package: `apk update && apk add luci-app-shunt` (this pulls in the `shunt` backend as a dependency).
2. Make reverse path filtering loose and give the policy interface a masquerading firewall zone - both once, both shown under [Prerequisites](#prerequisites). Without the first, marked traffic is dropped; without the second it is marked and routed and then goes nowhere, which looks exactly like shunt not working.
4. Open LuCI under `Services -> shunt`, add a policy on the `Policies` tab: pick the `Interface`, name the clients under `Source addresses` or `Source MAC addresses`, and list the `Domains` you want routed.
5. Start and verify:

```sh
/etc/init.d/shunt enable
/etc/init.d/shunt start
shunt check
```

**Please note:** a domain policy only takes effect for a client's *next* connection to an address that has just been learned - see [Known limitations](#known-limitations).

<a id="main-features"></a>
## Main Features
* Routes by client address, client MAC, ingress device, destination CIDR, port and domain, in any combination
* Resolver independent: works with any DNS backend, and with an encrypted upstream, because it reads the plaintext leg between client and resolver
* Wildcard domains (`*.example.com`), learned passively as clients use them
* Domain and address lists from a file - a community-maintained set of thousands of names or networks, without a single UCI entry per line
* Per-policy killswitch: reject the traffic when the interface or proxy is gone, instead of leaking it out of the normal uplink - clients fail at once rather than on a timeout
* Per-policy action: `route` marks the traffic for the policy interface, `bypass` exempts it from every policy below, `tproxy` hands it to a transparent proxy on the router
* Your own networks stay reachable from a policy client, without listing them anywhere
* Puts its own table back when something deletes it, `fw4 flush` included
* Own nftables table and routing tables, disjoint mark range - runs beside `pbr` and `mwan3`
* IPv4 and IPv6 throughout, with a MAC selecting a host in both at once
* Per-element counters on every set, a ubus status object and a LuCI frontend
* No dependency on a specific DNS backend, no resolver configuration, no include files, no hooks into fw4's ruleset

<a id="prerequisites"></a>
## Prerequisites
* OpenWrt with fw4/nftables
* `ucode` plus `ucode-mod-fs`, `ucode-mod-socket`, `ucode-mod-uci`, `ucode-mod-uloop`, `ucode-mod-resolv`, `ucode-mod-ubus`, `ucode-mod-rtnl`, `ucode-mod-log` and `rpcd-mod-ucode` - all pulled in by the package; `ip` is not needed, routes and rules are written over rtnetlink

`kmod-nft-tproxy` only for a `tproxy` policy. It is not pulled in: without it a tproxy policy is skipped as an issue and everything else is applied.

`ucode-mod-resolv` and `ucode-mod-ubus` are soft at runtime: without resolv, poll is skipped and the observer carries the service alone; without ubus, gateway discovery and interface events are skipped and the config's own values are used. Both cost one warning in the log, not a failed start.

<a id="which-tunnels-work"></a>
### Which tunnels work

Any of them, and there is no supported-protocols list to check against, because shunt never asks what protocol an interface speaks. It consumes two things: the device to route into, and a gateway if one is needed. Both come from netifd, and a device netifd does not manage is taken as given.

* **Point to point tunnels** - wireguard, OpenVPN `tun*`, L2TP, PPTP, Tailscale, NetBird and the like - need no gateway at all. The route is `default dev <device> table <n>`.
* **Ethernet style interfaces** - OpenVPN `tap*`, a second wired uplink, a mobile connection - use the gateway discovered from netifd, or `gw4`/`gw6` if you set them.
* **Interfaces netifd does not manage** work by name too. On OpenWrt this is the exception rather than the rule - wireguard, OpenVPN, L2TP and the rest all have netifd protocols and are managed like any other interface. It applies to a tunnel brought up outside netifd, by `wg-quick` or a script of your own. If such an interface does need a gateway, discovery cannot find one and you have to set `gw4`/`gw6` yourself.

**WireGuard: `route_allowed_ips` belongs off, unless the tunnel is the default uplink.** shunt renders `default dev <device>` into the policy's own table, so it needs no route from the tunnel itself. With the option on and `allowed_ips 0.0.0.0/0` netifd installs that prefix as a route in `main` instead, and the whole box follows the tunnel - the situation policy routing exists to avoid. Turning it off is all that is required; nothing else about the peer changes. Where the tunnel is meant to carry everything and the policies carve out the exceptions, leave it on - see [Default uplink and exceptions](#default-uplink-and-exceptions).

**`AllowedIPs` is not a route, and it still applies.** `route_allowed_ips` decides only whether netifd installs routes for those prefixes - they are written to the wireguard configuration either way, and the kernel uses them as its cryptokey filter. A packet whose destination matches no peer's `AllowedIPs` is dropped inside wireguard, however correctly it was routed there. A policy meant to carry general traffic therefore needs `0.0.0.0/0` on the peer, and `::/0` if it carries IPv6; what actually enters the tunnel is decided by the policy's selectors, not by the peer. Narrowing `AllowedIPs` to a few subnets is right only when those subnets really are everything the tunnel should ever carry.

The one thing that does not work is anything that is not a routable interface. Tor is the usual example: it normally offers a SOCKS port, and sending traffic there is a redirect, not a route. shunt marks a packet and looks up a routing table; without a device to put a default route on, there is nothing for it to do.

Two kernel-side prerequisites. shunt never changes either one behind your back - the first can be handed to shunt explicitly (`rp_filter_manage`, below), the second stays with fw4:

**`rp_filter` must be loose on the policy interface.** Marked traffic takes an asymmetric path, so strict reverse path filtering drops it. The kernel decides per incoming packet using `max(net.ipv4.conf.all.rp_filter, net.ipv4.conf.<dev>.rp_filter)`, where `2` is loose - so setting the policy interface alone to `2` suffices even while `all` stays strict. Prefer this: it leaves reverse path filtering intact on every other interface.

```sh
cat > /etc/sysctl.d/99-shunt.conf <<'EOF'
net.ipv4.conf.phy0-sta0.rp_filter=2
EOF
sysctl -p /etc/sysctl.d/99-shunt.conf
```

Replace `phy0-sta0` with your policy interface's device - the `Interface` column on the overview shows it - one line per policy device.

There is a boot-order catch. A device that does not exist yet - a tunnel, or a wifi client interface brought up late - has no `conf/<dev>` entry at boot, so `sysctl -p` cannot set it and skips the line. When the device finally appears it inherits `net.ipv4.conf.default.rp_filter`, and if that is strict the device comes up strict and stays that way until the next `sysctl -p` - which for most setups means until the next reboot, i.e. never in practice. Two ways around it: set `net.ipv4.conf.default.rp_filter=2` as well, which makes every later-appearing interface inherit loose (a little broader, but far short of `all`), or let shunt handle it with the option below.

**`rp_filter_manage` (optional, off by default).** With it set, shunt itself sets `rp_filter=2` on its own policy devices - at start and again whenever one comes up, which is exactly the boot-order moment a static file misses. It only ever touches the devices shunt routes into, never `all` or `default`, and only while the service runs. It is off by default because changing a security setting should be a deliberate choice:

```sh
uci set shunt.@global[0].rp_filter_manage='1'
uci commit shunt
/etc/init.d/shunt restart
```

Whichever way you choose, the daemon checks the live per-device values - at start and again whenever a policy interface comes up - and warns, naming the device, only when a policy device exists and is still strict. A device that is not there yet carries no traffic and triggers no warning; it is checked the moment it appears. With `rp_filter_manage` on the warning therefore simply does not appear - not because the switch is set, but because the values read back are actually loose. With `all` at `2` (or `0`) nothing is ever reported. The package deliberately ships no box-wide sysctl file: `rp_filter` on `all`/`default` is a distribution default OpenWrt sets strict in `/etc/sysctl.d/10-default.conf`, and loosening it there weakens anti-spoofing on every interface, well beyond shunt's own traffic.

**Masquerading stays fw4's job.** shunt marks and routes; it does not touch the firewall's NAT. The policy interface needs a zone with `masq` enabled and forwarding from `lan`, exactly as any other uplink. If the interface is a netifd one - say a wireguard interface named `vpn`:

```sh
uci add firewall zone
uci set firewall.@zone[-1].name='vpn'
uci set firewall.@zone[-1].input='REJECT'
uci set firewall.@zone[-1].output='ACCEPT'
uci set firewall.@zone[-1].forward='REJECT'
uci set firewall.@zone[-1].masq='1'
uci set firewall.@zone[-1].mtu_fix='1'
uci add_list firewall.@zone[-1].network='vpn'

uci add firewall forwarding
uci set firewall.@forwarding[-1].src='lan'
uci set firewall.@forwarding[-1].dest='vpn'

uci commit firewall
/etc/init.d/firewall reload
```

`network` names a **logical interface**, not a device. For a device netifd does not manage - a tunnel brought up outside netifd - use `uci add_list firewall.@zone[-1].device='wg0'` instead. And if the policy interface already has a zone, because it is an ordinary second uplink, there is nothing to do here.

Symptoms of getting this wrong are worth knowing, because they do not look like a firewall problem: the prerouting counters rise, `nft list set` shows the learned address being hit, and the client's connection simply times out.

<a id="installation-and-usage"></a>
## Installation and Usage
* Update your router's apk repository (`apk update`)
* Install the LuCI companion package `luci-app-shunt`, which also installs the main `shunt` package as a dependency
* Make `rp_filter` loose and give the policy interface a masquerading firewall zone - both are one-time steps with copy-paste commands under [Prerequisites](#prerequisites)
* Configure at least one policy, either in LuCI under `Services -> shunt` or by editing `/etc/config/shunt`
* Enable and start the service, then run `shunt check` - it prints the mark, routing table and rule priority of every accepted policy, a bypass policy by name alone, and every rejected value with its reason
* Check the `Set Reporting` tab to see which addresses were learned, and the `Processing Log` tab for the service's own messages

<a id="shunt-cli-interface"></a>
## shunt CLI interface
All functions are available from the command line, and the config file can be edited directly if you prefer that to LuCI.

```sh
shunt check        # render everything, print marks and issues, change nothing
shunt run          # foreground, the procd service entry point
shunt flush        # tear down table, rules, routes and the mapping file
shunt refresh      # make the running daemon re-read its list files
shunt -v <cmd>     # echo every message to the terminal as well
```

`shunt check` is safe at any time, including while the service runs, because it only renders - it never touches the kernel. Run it after every config change. Note that it says nothing about whether the service is *running*; that is what `/etc/init.d/shunt status` and the LuCI overview are for.

Exit codes: 0 ok, 1 runtime failure, 2 usage or unusable config.

Logging goes to syslog under the tag `shunt`, so `logread -e shunt` shows everything - the daemon's own lines carry its pid, `shunt[1234]:`. Debug lines stay off unless `-v` is given or `option debug '1'` is set - under procd there is no command line, so a bug report needs the config switch. Expect volume: on a router running adblock roughly half of all observed answers are error replies, and debug gives each one a line.

`shunt flush` is the escape hatch if the daemon ever dies without tearing down. It is idempotent and safe on a box that never ran shunt.

`shunt refresh` asks the daemon over ubus to re-read every `domain_file` and `dst_file` and reports what it found; it is the one change that does not need a restart. Without a running daemon it says so and exits 1 - nothing is lost, the next start reads the files anyway. Everything else in the config still takes `/etc/init.d/shunt restart`.

<a id="shunt-config-options"></a>
## shunt config options

### Global section

| Option | Default | Description |
| :--- | :--- | :--- |
| enabled | `1` | master switch; `0` means the service starts and exits |
| debug | `0` | log every observed answer and every set write |
| rp_filter_manage | `0` | set rp_filter=2 on shunt's own policy devices, at start and on ifup |
| poll_interval | `300` | seconds between poll cycles, at least 30 |
| entry_ttl | `1200` | ceiling for the lifetime of learned elements, at least 60 |
| snoop | `1` | enable the passive DNS observer |
| snoop_device | `br-lan` | LAN devices to observe, a list, one entry per segment |

Values below the minimum are clamped, not rejected, and the clamp is logged. `entry_ttl` should stay well above `poll_interval` - a polled element is rewritten once its remaining timeout drops below half of `entry_ttl`, so the default pair refreshes comfortably within two poll cycles. Elements learned by snoop expire with the TTL of the answer instead, see [How addresses are learned](#how-addresses-are-learned).

### Policy sections

Each `config policy` section is one routing policy. **The section must be named, and the name must match `[A-Za-z0-9_]{1,24}`** - it becomes an nftables identifier, so a section without a name, or one with a hyphen or a dot in it, is rejected as an issue and never rendered. LuCI enforces the same pattern when a policy is added.

| Option | Description |
| :--- | :--- |
| enabled | `0` skips the section entirely |
| action | `route` (default), `bypass` or `tproxy`, see below |
| interface | netifd logical name (`wan`, `trm_wwan`) or raw netdev (`wg0`, `phy0-sta0`); `route` only |
| tproxy_port | port the transparent proxy listens on; `tproxy` only, required there |
| fallback | `main` (default) or `block`, see below; `route` and `tproxy` |
| keep_local | `1` (default) keeps traffic on `main` where `main` has a route for it, see below; `route` only |
| gw4 / gw6 | gateway override; normally unnecessary |
| src | client addresses or CIDRs whose traffic this policy owns |
| src_mac | client MAC addresses, ORed with `src` |
| src_dev | ingress devices (`br-guest`, `lan2`), ORed with `src` and `src_mac`, see below |
| proto | `tcp`, `udp`, or both; a port without one covers both |
| dport | destination ports, single or a range like `8000-8080` |
| dst | destination addresses or CIDRs |
| dst_file | files with destination addresses or CIDRs, one per line, see below |
| domain | domain patterns, see below |
| domain_file | files with domain patterns, one per line, see below |

`src`, `src_mac`, `src_dev`, `dst`, `dst_file`, `domain` and `domain_file` are lists and may repeat.

Interfaces are resolved through netifd: a logical name resolves to its `l3_device`, a raw netdev is adopted if netifd knows it, and a device netifd knows nothing about passes through as given - which on OpenWrt means a tunnel started outside netifd, since wireguard and the other tunnel types have netifd protocols of their own. Gateways are discovered from the same dump, merged across sibling entries, because netifd splits families. `gw4`/`gw6` override discovery and always win; on a point to point interface no gateway is needed at all.

There is deliberately no list of supported tunnel protocols. shunt asks netifd for the device and the gateway and renders a default route into the policy table - `default via <gw> dev <device>` when a gateway is known, `default dev <device>` when none is needed. Wireguard, OpenVPN in both `tun` and `tap` mode, L2TP, PPTP, Tailscale, NetBird, a second physical uplink or a mobile connection all reduce to those two shapes, so none of them needs a case of its own. See [Which tunnels work](#which-tunnels-work) for the one thing that genuinely does not fit.

### Selectors are ANDed, client selectors OR each other

* `src`, `src_mac` or `src_dev` alone marks everything from those clients
* `dst`, `dst_file`, `domain`, `dport` or `proto` alone marks that traffic from everyone - `dport '25'` by itself routes everybody's smtp
* clients plus destinations marks only those clients' traffic to those destinations

`dport` and `proto` AND with everything else, so a policy with a client, a domain and `dport 443` covers that client's HTTPS traffic to that domain and nothing more. A port without a protocol matches **both** tcp and udp - "port 443" almost always means QUIC too, and requiring the protocol would let it slip through unnoticed. If ports or protocols were configured and none of them is usable, the policy is skipped rather than rendered without the narrowing.

This is the single most common source of "it did not work" reports: with a client and a domain both set, a generic `curl ifconfig.me` from that client correctly takes the normal uplink, because `ifconfig.me` is not in the domain list. That is the policy working, not failing.

A client MAC, a client address and an ingress device OR each other, so a host may be named either way. If client selectors were configured and **none** of them is usable - a typo in the only address, say - the policy is skipped with an issue rather than falling back to "every client", which is what an absent client selector otherwise means. The same holds for `dst`: if every destination address is unusable and no domain is left to narrow the policy, it is skipped rather than widened to every destination.

### Selecting clients by MAC

`src_mac` exists mainly for IPv6. Clients prefer rotating privacy addresses for outgoing traffic, so a single IPv6 address is not a usable selector and the LAN prefix covers every host in the segment. A MAC picks exactly one host, in both address families, and keeps doing so when the addresses change. A policy with only `src_mac` therefore needs no v6 address to route v6.

Three limits, none of them guessable:

* **Same layer 2 segment only.** Anything behind another router arrives with that router's MAC.
* **Never the router itself.** The `output` chain sees traffic the router generated, which has no ethernet sender, so MAC rules are not installed there. An address based policy does cover the router; a MAC-only one does not.
* **Phones randomise their MAC**, though usually stable per network. Use the address the client shows in your DHCP leases, not the one on the label.

<a id="selecting-clients-by-ingress-device"></a>
### Selecting clients by ingress device

`src_dev` selects everything that arrives on a device - a whole guest network, an IoT VLAN, a second LAN port - in both address families at once. That is what an address selector cannot do for IPv6 on a line with a dynamic prefix: the delegated prefix changes, and the prefix you configured stops matching the day it does. The device does not change.

```
config policy 'guest'
	option interface 'wg0'
	option fallback  'block'
	list   src_dev   'br-guest'
```

The entry is a kernel device name - `br-guest`, `lan2`, `eth0.20` - as `ip link` shows it, not a netifd interface name. It is matched by name, so a device that does not exist yet is no error: the rule matches nothing until it appears. Like `src_mac`, it exists in prerouting only, so the router's own traffic is never covered by it.

With `keep_local` at its default the devices' traffic to your other networks stays local, and traffic to the router itself - DNS, DHCP, LuCI - is delivered locally whatever the mark says. For domain policies the device has to be observed as well: add it to `snoop_device`, or names its clients ask for are learned by poll alone.

### Domain patterns

```
example.com      matches the apex only
*.example.com    matches subdomains only, at any depth, NOT the apex
```

List both to cover both. This is more typing than dnsmasq's implicit subdomain inclusion, and it is deliberate: dnsmasq's behaviour surprises people regularly, this one does not.

Precedence, in order:

1. an exact match always beats any wildcard
2. among wildcards the longest suffix wins, so `*.cdn.example.com` beats `*.example.com` regardless of which policy declared them
3. the same pattern in two policies belongs to **both**

Rule 3 is what makes one domain usable by two client groups over two different uplinks: the address is written into each policy's set, and each policy's rule matches only its own clients, so they stay apart. Rules 1 and 2 still decide specificity - a shared `*.example.com` never overrides somebody's exact `www.example.com`.

Matching is label aligned, never string suffix: `evilexample.com` does not match `*.example.com`. A bad pattern is collected as an issue, never fatal.

<a id="domain-lists-from-a-file"></a>
### Domain lists from a file

A `list domain` entry is right for a handful of names. A community-maintained list has thousands, and the place for those is a file, not UCI:

```
config policy 'vpn'
	option interface   'wg0'
	list  src          '192.168.1.0/24'
	list  domain       'my-own-exception.example'
	list  domain_file  'community.txt'
```

A bare name lives under `/tmp/shunt/`, which shunt creates at start; an absolute path is taken as given, for a list kept on a mounted drive. The default is tmpfs on purpose: a list of that size rewritten every day belongs in RAM, not on the flash the overlay sits on. The price is that after a reboot the file is not there until whatever produces it has run again, and that is handled, not worked around - see below.

The file holds one pattern per line in the same two shapes as `list domain` - `example.com` or `*.example.com` - with `#` starting a comment and blank lines ignored. It is read at start, validated by the same rules, and its patterns join the policy's own; `domain` and `domain_file` are additive, and a file may be named by more than one policy. A file that is not there, is unreadable or holds no usable pattern is reported as an issue against the policy and contributes nothing; the policy itself is still rendered, so with `domain_file` as its only domain selector its learned sets simply stay empty until the file arrives. Bad lines are counted and the first one is named, rather than logging every one of them.

Two things distinguish a file entry from a `list domain` one, both deliberate:

* **File entries are learned by snoop only.** poll resolves the names you typed, not the ones from a file - ten thousand names every `poll_interval` is a DNS storm, not a service. A name from a file reaches its set when a client on the observed LAN segment asks for it, which for a large list is exactly when it matters and never before. The [first-connection caveat](#known-limitations) applies to every one of them.
* **shunt reads the file, it does not fetch it.** Where the list comes from and how often it changes is a job for cron, a script or a package of its own, and whatever writes the file runs `shunt refresh` afterwards: the daemon re-reads every domain file and swaps its matcher, and nothing else moves - no restart, the nftables table, the routes and the learned addresses all stay as they are. Write the new list to a temporary name and rename it over the old one, so a half-written file is never read:

```sh
uclient-fetch -q -O /tmp/shunt/community.tmp https://example.org/domains.txt && mv /tmp/shunt/community.tmp /tmp/shunt/community.txt && shunt refresh
```

Run from cron that covers the reboot case as well: the first run after boot creates the file and the refresh loads it. That keeps shunt independent of the network, of any download tool and of the format a list publisher happens to choose - converting `hosts` or dnsmasq syntax into one pattern per line is one `sed` away and stays outside the daemon.

What a large list costs: 100k patterns take about two seconds to load and roughly 40 MB of memory on an x86 test box, so expect several seconds and a proportionally smaller footprint on a router, at start and on every refresh. A file above 4 MiB is refused outright as an issue. `shunt check` prints how many files were read and how many patterns they contributed, `shunt refresh` does the same for the running daemon, and `ubus call shunt status` shows the count under `files` together with the time of the last refresh.

<a id="address-lists-from-a-file"></a>
### Address lists from a file

`dst_file` is the same mechanism for destinations: a provider's published ranges, a country list, a set of CDN networks - thousands of prefixes that have no business in UCI.

```
config policy 'streaming'
	option interface 'wg0'
	list   src       '192.168.1.0/24'
	list   dst_file  'provider.txt'
```

The file holds one IPv4 or IPv6 address or prefix per line, both families mixed as they come, with `#` starting a comment and blank lines ignored. Name, location, size limit, issues and `shunt refresh` work exactly as for a [domain file](#domain-lists-from-a-file), and `dst` and `dst_file` are additive. Every entry is checked by the rules a `list dst` entry is checked by, so a bad line costs that line and never the ruleset.

Overlapping prefixes are fine: the sets are created with `auto-merge`, so `10.0.0.0/8` next to `10.1.0.0/16` is folded into one instead of failing the whole batch. The policy gets its static sets for both families even while the file is missing, so a later file may bring either family. `shunt refresh` flushes and refills those sets in one nftables transaction - the set is never seen half-filled - and leaves everything else in place.

A list of 100k prefixes takes about two seconds to read and roughly 40 MB of memory on an x86 test box, and nftables about two seconds to load it, at start and on every refresh; expect several times that on a router.

### Actions: route, bypass or tproxy

`action 'route'` (default) is the policy shape everything above describes: matching traffic is marked and looked up in the policy's own table.

`action 'bypass'` marks nothing. Its rule ends evaluation and the packet leaves shunt untouched, which is the equivalent of pbr's `ignore`: since a packet takes the first policy it matches, a bypass section carves an exception out of every policy **below** it. It needs no `interface`, consumes no mark, and gets no routing table or ip rule of its own - `fallback`, `keep_local` and the gateway overrides are not read.

The selectors are the same ones a routing policy uses, domains included, so an exception can be as narrow as one client's traffic to one name:

```
config policy 'no_vpn'
	option action 'bypass'
	list  src     '192.168.1.50'
	list  domain  'bank.example.com'

config policy 'vpn'
	option interface 'wg0'
	list  src        '192.168.1.50'
```

Order matters and only order: a bypass section placed after the policy it is meant to except from never sees the packet.

<a id="transparent-proxy-tproxy"></a>
### Transparent proxy: tproxy

`action 'tproxy'` hands the selected traffic to a transparent proxy running on the router - `hev-socks5-tproxy`, `sing-box` or `xray` in tproxy mode, or anything else that accepts `IP_TRANSPARENT` connections. shunt only steers: it does not start, configure or watch the proxy. It needs `kmod-nft-tproxy`.

```
config policy 'proxy'
	option action      'tproxy'
	option tproxy_port '1088'
	option fallback    'block'
	list   src         '192.168.1.0/24'
	list   domain      '*.example.com'
```

The selectors are the usual ones. tproxy carries tcp and udp only, so a tproxy policy without `proto` covers both, and the proxy must listen for every protocol the policy selects. A policy consumes a mark and gets a routing table holding a single `local default dev lo` route - that is how the selected packets reach a socket on the router without their destination being rewritten.

shunt delivers to `127.0.0.1` and `::1`. A proxy listening on loopback or on the wildcard address is found; one bound to the router's LAN address is not. The address is not left to the kernel on purpose: without one it picks the address of the incoming interface, and a proxy bound to loopback would never be found - the statement fails silently and the traffic is not proxied.

Each family needs a listener that accepts it. A proxy on `0.0.0.0` or `127.0.0.1` serves IPv4 only, so the IPv6 traffic of the policy finds no listener and follows `fallback`. Listen on `::` - which takes both families unless the proxy sets `IPV6_V6ONLY` - or on both loopback addresses.

Destinations on the router itself are never proxied, nor broadcast and multicast: DNS, DHCP, LuCI and SSH of a selected client reach the router as they would without shunt, and stay reachable with `fallback 'block'` while the proxy is down. This is tproxy's counterpart to `keep_local`, built in and not switchable. It covers the router's own addresses only - traffic to your other networks is forwarded unicast and is proxied like any other. A proxy on the router cannot reach those networks on the client's behalf in a useful way, so exempt them with a `bypass` section above the tproxy policy:

```
config policy 'no_proxy'
	option action 'bypass'
	list  dst     '192.168.0.0/16'
	list  dst     'fd00::/8'
```

`fallback` decides what a **new** flow does when no listener is found - proxy stopped, crashed, wrong port: `main` (default) lets it take the normal uplink, `block` rejects it - tcp with a reset, anything else with ICMP admin-prohibited, so the client gives up at once instead of on a timeout. A flow that was already proxied is rejected the same way once its listener is gone, whatever the fallback. Its state is in the proxy, and its conntrack entry was never NATed, so forwarding the rest of it would put the client's own address on the uplink - measured, which is why this does not follow `fallback`.

Without `proto` or `dport` the policy owns every protocol its clients send, not only what a proxy can take. `fallback 'block'` therefore also rejects ICMP, ESP, GRE and the rest - while the proxy runs too, so `ping` from those clients never gets out. Router addresses, broadcast and multicast stay exempt as above, which keeps neighbour discovery and the router's own services working. With `proto` or `dport` the policy owns only what it names and `block` rejects only that; everything else the client sends - QUIC for a policy narrowed to `proto 'tcp'` - takes the normal uplink.

`interface`, `keep_local` and the gateway overrides are not read, and `keep_local '1'` set explicitly is reported - the exemption of the router's own addresses above takes its place. It would break the policy: main would forward packets the kernel has already handed to the proxy's socket, and the kernel drops those.

Only traffic passing through the router is proxied. tproxy exists in the prerouting hook alone, so the router's own traffic is not touched by a tproxy policy - which also keeps the proxy's upstream connections from looping back into it.

The proxied traffic is delivered to the router itself, so fw4 applies its **input** rules, not its forward rules. On `lan`, with input `ACCEPT`, nothing needs doing. A zone with input `REJECT` or `DROP` - a guest network, typically - drops it, and shunt cannot override that from its own table, because a drop in any base chain is final. The packet still carries its original destination port, so a rule on the proxy port does not match; match the policy's mark instead (`shunt check` prints it):

```
config rule
	option name   'Allow-guest-tproxy'
	option src    'guest'
	option mark   '0x01000000/0xff000000'
	option target 'ACCEPT'
```

**Example backend: hev-socks5-tproxy.** It turns tproxy traffic into connections to a SOCKS5 server and is packaged for OpenWrt. Its sample `/etc/hev-socks5-tproxy/main.yml` already fits the policy above - tcp and udp on `::` port 1088, the SOCKS5 server expected on `127.0.0.1:1080`:

```
socks5:
  port: 1080
  address: 127.0.0.1
  udp: 'udp'

tcp:
  port: 1088
  address: '::'

udp:
  port: 1088
  address: '::'
```

Point `socks5` at your server, then enable the service with `uci set hev-socks5-tproxy.config.enabled='1'`, `uci commit hev-socks5-tproxy` and `/etc/init.d/hev-socks5-tproxy start`. Its own connections to the SOCKS5 server start on the router, so the tproxy policy never sees them; a `route` policy can still send them through a tunnel.

<a id="local-traffic-keep_local"></a>
### Local traffic: keep_local

A policy that names only clients marks everything those clients send, and the mark decides the routing table before the destination is looked at. Without `keep_local` that includes traffic to your own networks: a marked packet to another VLAN would find only the policy table's default route and leave through the tunnel, and with `fallback 'block'` it would be rejected.

`keep_local '1'` (default) renders a second ip rule per family, on the same mark and one priority ahead of the policy rule, that consults `main` with the default route suppressed:

```
ip rule add pref 31001 fwmark 0x1000000/0xff000000 lookup main suppress_prefixlength 0
ip rule add pref 31501 fwmark 0x1000000/0xff000000 lookup 8001
```

Anything `main` has a **specific** route for - every attached subnet, every static route - keeps taking it; everything that would have used the default route falls through to the policy rule. The policy itself is unaffected, because its whole purpose is the default route it renders into its own table.

`keep_local '0'` renders the policy rule alone. That is the hermetic form: with `fallback 'block'` nothing marked can leave through another interface, at the price of the local traffic above. Set it only where a leak matters more than reaching your own networks, and expect to name the destinations you still want reachable in a `bypass` section instead.

### Policy precedence

Section order in `/etc/config/shunt`, top to bottom. There is no `priority` option - one less value to set wrong. A packet matching two policies takes the earlier one; rule evaluation ends at the first match.

Note that domain precedence is resolved *before* this, at the matcher: the most specific pattern wins even if it sits in a later section.

### Fallback: main or block

`fallback 'main'` (default) renders no default route into the policy table, so when the policy interface is down the table is empty and marked traffic falls through to `main` - the normal uplink. Traffic keeps flowing, unpolicied.

`fallback 'block'` adds a `prohibit` default at metric 9999 to the policy table. While the interface is up its own default has the lower metric and wins; when the interface drops, the kernel withdraws that route and the prohibit route catches everything, answering it with ICMP admin-prohibited - a connection attempt fails at once instead of on a timeout, measured. That is the killswitch: traffic belonging to the policy stops rather than leaking out of the wrong interface. It stops at the destinations the policy actually owns - see [keep_local](#local-traffic-keep_local) for what a marked packet to one of your own networks does, and how to make the killswitch hermetic.

<a id="default-uplink-and-exceptions"></a>
### Default uplink and exceptions

shunt routes only what its policies select. Everything else follows `main`, and which uplink `main` uses is decided by the interface metrics, not by shunt. A box with a normal uplink and a tunnel can therefore be set up either way round:

**ISP as default, selected clients through the tunnel.** The uplink keeps its default route in `main`, the tunnel installs none (`route_allowed_ips '0'`). The policy names the tunnel as its `interface`:

```
config policy 'vpn'
	option interface 'wg0'
	list  src_mac    'aa:bb:cc:dd:ee:ff'
```

**Tunnel as default, selected clients through the ISP.** The tunnel carries the default route in `main` (`route_allowed_ips '1'` and a lower `metric` than the uplink), and the policy names the uplink as its `interface`:

```
config policy 'isp'
	option interface 'wan'
	list  src_mac    'aa:bb:cc:dd:ee:ff'
```

In both forms the policy's own table decides for its clients in both address families, so the metrics matter only for traffic no policy selects. Set them explicitly on both sides anyway, including `wan6` where IPv6 comes from an interface of its own: netifd's default `metric 0` stays 0 for IPv4, but the kernel stores an IPv6 route with metric 0 as 1024. An interface left at the default therefore wins the IPv4 default route and loses the IPv6 one against any interface with an explicit metric - unselected clients then leave through different uplinks per family.

`fallback 'block'` covers the policy's clients only. In the tunnel-as-default form, unselected clients move to the uplink when the tunnel goes down, because netifd withdraws the tunnel's route from `main` and the uplink's is left. Preventing that is a matter of the firewall, not of shunt.

<a id="how-addresses-are-learned"></a>
## How addresses are learned
Two sources feed the same nftables sets, union with an element timeout. They are complementary, not alternative modes.

* **poll** resolves the configured names through whatever system resolver exists, on a fixed interval. It warms the sets before the first client packet, so first contact does not race. Wildcards are not names and cannot be polled, and names from a `domain_file` are not polled by design - see [Domain lists from a file](#domain-lists-from-a-file).
* **snoop** passively observes DNS responses on the LAN side via AF_PACKET with a BPF filter matching **UDP source port 53** - answers, not questions - including one level of VLAN tagging. It covers CDN variance and wildcards, which poll cannot. It reads; it never writes anything back onto the wire and never sits between a client and its resolver. If it dies, DNS keeps working and only the policy stops applying.

The two sources differ in how long an element lives. A snooped element carries the TTL of the answer it came from - the shortest one, when a message mixes several - clamped to the range 60 to `entry_ttl` seconds. So an address a CDN hands out for 30 seconds is gone from the set about a minute after the last answer that named it, while a name with a day-long TTL is capped at `entry_ttl` and re-learned on its next answer. A polled element carries `entry_ttl`: the resolver interface hands back addresses without their TTL, so poll has nothing better than the ceiling. Where both sources see the same address the shorter bound wins - the next snooped answer cuts a polled element down to its TTL, and the write cache only rewrites an element when the new expiry moves by at least half of the lifetime the answer carries, so a burst of identical answers costs one write.

The kernel timeout runs a few seconds past that lifetime: the current write interval plus two seconds, so four on an ordinary box. A client asks again when the TTL runs out, and the write for the new answer reaches the set up to one write interval later; an element timed out to the second would be missing for exactly that gap, and a connection opened in it would start on the normal uplink and stay there. The slack follows the interval because the gap does - on a box where writes are expensive and the interval has grown, see [What shunt creates on the system](#what-shunt-creates-on-the-system), elements outlive their TTL by correspondingly more.

This is also the honest answer to the question every IP-based policy router gets: what about a CDN address that the domain stops using while some other site starts to? Nothing on layer 3 can tell two names apart once they share an address, shunt included. What shunt can do is not keep the address longer than the resolver would have, which is exactly what the TTL says.

<a id="what-polling-costs"></a>
### What polling costs

"Polling" invites the assumption of waste, so here is the arithmetic. One cycle is a single call asking for A and AAAA of every listed name: two lookups per name per interval, against the **local** resolver. Ten names at the default 300 seconds is 240 lookups an hour - about what a dozen web page loads cost, on a network whose own DNS traffic runs to hundreds of answers in a few minutes. There is no polling of anything else: no interface scanning, no ruleset re-rendering, no periodic writes. A polled element is only rewritten when its remaining lifetime has dropped below half.

Two costs worth knowing:

* The query is synchronous. A name that does not resolve blocks the cycle until it times out (2s, one retry), which delays the service start noticeably if several are wrong. This is why an unresolvable name is reported by name.
* For names with a TTL shorter than the interval the local cache has expired, so poll does refetch upstream rather than answering from cache.

What that means in practice:

* **Pick the device the answers cross on their way to the clients**, normally `br-lan`. It must be an **Ethernet type** device - a bridge, a VLAN device, a physical port, a wireless interface. A tunnel or PPP interface has no ethernet header, so neither the packet filter nor the decoder can read it, and the failure is silent: nothing matches, nothing is logged.
* **Only the client-to-resolver leg matters, and only whether *it* is encrypted.** What the resolver does upstream is irrelevant: the usual OpenWrt setup - unbound or dnsmasq on the router, forwarding upstream over DoT or DoH
  - is fully covered, because the client asked in plain text over the LAN and the answer comes back the same way.
* **A client that speaks DoH or DoT itself is invisible**, because it bypasses the local resolver. That is the one encryption case that costs coverage.
* **One entry per layer 2 segment.** A guest or IoT VLAN on its own device never carries the answers of the main LAN, so it needs its own `snoop_device` entry. A device that cannot be opened costs one warning; the others keep running.
* **The router's own lookups are not seen.** poll's queries leave through the uplink, not the LAN device.
* Not seen either: DNS over TCP, DNS on a port other than 53, and a second stacked VLAN tag.

A name in a `domain` list that never resolves is reported once, by name and policy:

```
poll: www.example.com (policy vpn) has no address - the policy entry has no effect until it resolves
```

Once on the way in and once on recovery, never in between. The first cycle runs immediately at start, so a typo shows up within seconds. Wildcards cannot produce this message - nothing can tell whether `*.example.com` was ever meant to match anything.

<a id="examples"></a>
## Examples

**One client, one domain family, over a wireguard tunnel**

```
config policy 'vpn'
	option enabled   '1'
	option interface 'wg0'
	option fallback  'main'
	list  src        '192.168.1.50'
	list  domain     'example.com'
	list  domain     '*.example.com'
```

**A whole IoT VLAN over a mobile uplink, killswitch on**

The client is named by MAC, so it is covered in both address families without listing a rotating IPv6 address:

```
config global
	list  snoop_device 'br-lan'
	list  snoop_device 'br-iot'

config policy 'iot'
	option enabled   '1'
	option interface 'trm_wwan'
	option fallback  'block'
	list  src_mac    'aa:bb:cc:dd:ee:ff'
	list  domain     '*.vendor-cloud.com'
```

**The same domain for two client groups over two uplinks**

Both policies claim `www.example.com`; each routes only its own clients:

```
config policy 'wwan'
	option interface 'trm_wwan'
	list  src        '10.168.30.70'
	list  domain     'www.example.com'

config policy 'vpn'
	option interface 'wg0'
	list  src        '10.168.1.20'
	list  domain     'www.example.com'
```

**An exception for one client, over a policy that covers the whole subnet**

The bypass section comes first, so the packets it matches never reach the policy below it. Everything else from the subnet goes into the tunnel:

```
config policy 'no_vpn'
	option action 'bypass'
	list  src     '192.168.1.50'
	list  domain  'bank.example.com'
	list  domain  '*.bank.example.com'

config policy 'subnet'
	option interface 'wg0'
	list  src        '192.168.1.0/24'
```

**A whole guest network into the tunnel**

`br-guest` is the device, not the netifd interface. It covers the guest network's IPv4 and IPv6 alike, whatever prefix the provider hands out:

```
config policy 'guest'
	option interface 'wg0'
	option fallback  'block'
	list   src_dev   'br-guest'
```

**One port for everybody**

No client, no destination - every smtp connection leaves through the second uplink:

```
config policy 'smtp'
	option interface 'wan2'
	list   dport     '25'
```

**A destination range without any domain**

```
config policy 'office'
	option interface 'wg0'
	list  src        '192.168.1.0/24'
	list  dst        '10.0.0.0/8'
```

<a id="what-it-shells-out-to"></a>
### What it shells out to

Almost nothing. The daemon and the rpcd backend work through ucode's native bindings - `fs`, `socket` for the AF_PACKET observer, `uci`, `uloop`, `ubus`, `resolv`, `rtnl` and `log` - and rpcd carries the LuCI side, so there is no shell glue, no `awk`, no temporary state files. Logging goes to syslog through the binding, not through a `logger` process per line.

One external command remains:

| Command | Why |
| :--- | :--- |
| `nft` | the ruleset is applied and read as one atomic batch; ucode has no nftables binding |

Routes and rules go over rtnetlink through `ucode-mod-rtnl`, the same binding the rpcd backend reads them with, so there is no `ip` process per command and no `ip` dependency. A route on a device that does not exist yet is reported by the kernel's own reason rather than an exit code, and `rp_filter_manage` writes the `/proc/sys` value directly instead of calling `sysctl`.

Nothing is ever handed to a shell for parsing: `system()` takes an argument array and only ever runs `nft`, and `popen()` only ever runs fixed command lines - `nft -f -` in the daemon and `nft -j list table` in the rpcd backend - so no configuration value or captured data reaches a command line.

<a id="what-shunt-creates-on-the-system"></a>
## What shunt creates on the system

```
table inet shunt                     own table, see below
  chain prerouting                   filter hook prerouting, priority mangle
  chain output                       route hook output, priority mangle
  set d4_<policy> / d6_<policy>      learned, flags timeout, per-element counter
  set s4_<policy> / s6_<policy>      static dst and dst_file, flags interval, auto-merge, counter
  set c4_<policy> / c6_<policy>      client src selectors, interval, auto-merge, counter
  set m_<policy>                     client MACs, no family digit, counter
  set i_<policy>                     client ingress devices, no family digit, counter

fwmark                               <index> << 24, mask 0xff000000
ct mark                              same bits, set on a flow's first packet
ip rule pref                         31000 + <index> keep_local's main lookup
                                     31500 + <index> the policy table
routing table                        8000 + <index>, for tproxy: local default dev lo
                                     with fallback 'block': prohibit default metric 9999
/etc/iproute2/rt_tables.d/shunt.conf the table name mapping, for `ip route show` only
```

A `bypass` policy is only a rule in the prerouting and output chains: no mark, no table, no ip rule, and it does not count against the 255. A `tproxy` policy has rules in prerouting only, and no keep_local rule. The mark mask is fixed at `0xff000000`, which allows 255 policies. The `output` chain is `type route` so the router's own marked traffic is re-routed after the mark is set.

Every set carries per-element counters, so "is this element ever hit" is one look at `nft list set inet shunt <set>` rather than a tcpdump session. The two kinds count different things: nftables tests a rule left to right, so a **client** set counts every packet that matched the selector, whether or not the destination matched afterwards; a **learned** set is the last lookup in the rule, so a hit there means the packet really was marked. A busy client beside learned addresses at zero is a client that has not visited any of the routed domains, not a fault.

**The table can be deleted from outside, and comes back.** `fw4 reload` and `fw4 restart` touch only `table inet fw4`, so shunt's table survives both. `fw4 flush` does not: it walks `nft list tables` and deletes every table it finds, shunt's included - and that is exactly what `/etc/init.d/firewall stop` runs. Nothing in the kernel reports the loss, so the daemon checks that its table is still there once per poll cycle, and immediately whenever a write into a learned set fails. If it is gone, the ruleset is re-applied and the write cache is dropped, so poll and snoop refill the learned sets rather than suppressing addresses the kernel no longer has:

```
shunt: ruleset gone from the kernel - re-applying; `fw4 flush`, as run by `/etc/init.d/firewall stop`, deletes every nftables table
shunt: ruleset re-applied - learned addresses refill from the next poll cycle and from snoop
```

The routes and ip rules are not affected by any of this - they are re-asserted every poll cycle anyway. Until the check runs, marked traffic simply is not marked, so it takes the normal uplink; with `fallback 'block'` that means the killswitch is not in force either. `ubus call shunt status` counts the recoveries as `reapplied`.

**Writes are batched, and the interval adapts.** `nft -f` reads the entire ruleset from the kernel before it resolves a single name, so on a box that also runs a tool with very large sets - banIP with 238k elements, measured - one `add element` costs seconds of CPU, and `nft --check` alone costs the same. That is a known bug in nftables (netfilter bugzilla #1735, open since 2024), not something shunt can fix, so observed addresses are collected and applied together by a timer.

The interval follows what the last write actually cost, between 2 and 60 seconds: on an ordinary box a write takes milliseconds and the interval stays at its floor, where the batching is invisible. Where it is expensive the interval grows until nftables takes a bounded share of the machine instead of all of it, at the price of a learned address reaching its set later. Both numbers show up under `debug`.

**A reload wipes learned state.** Applying the configuration destroys and re-creates the table atomically, so the learned sets start empty. poll rewarms them within one interval and snoop refills from live traffic; expect a short window after a restart where domain policies do not apply yet.

**A flow is routed once, on its first packet.** The mark a `route` policy sets is also written to the flow's conntrack entry, and every later packet of the flow in the original direction takes its mark from there and never reaches the set lookups; flows that got no mark, or a `bypass`, stay settled the same way. That is what makes a set change safe for connections already running: the kernel kills a masqueraded conntrack entry whose output interface changes, so re-marking a live flow would reset it - measured, not assumed. The same rule holds across a reload. Conntrack survives it, so a connection keeps the decision it started with, including the policy index encoded in its mark; if a reload reorders the policies, that index may now belong to a different policy. Existing connections are not re-evaluated against the new configuration, new connections follow it. Reboot, or restart the client's connections, if that matters after a reorder.

<a id="coexistence-with-pbr-and-mwan3"></a>
## Coexistence with pbr and mwan3
shunt is an independent implementation, not a fork of `pbr` and not a drop-in for it - there is no config migration and no attempt at feature parity. Within its scope it is a full alternative.

Running both at once during a migration is safe by construction:

| | pbr | mwan3 | shunt |
| :--- | :--- | :--- | :--- |
| fwmark and ct mark mask | `0x00ff0000` | `0x00003f00` | `0xff000000` |
| ip rule pref | 30000 counting down | ~1001-3250 | 31000 and 31500 counting up |
| routing tables | dynamic from ~256 | 1-250 | 8000+n |
| nft | chains in fw4's table | | own `inet shunt` table |

The mark bits are disjoint and all three mask their writes. Where pbr and shunt both match, pbr's lower rule priority wins, deterministically. So move policies over one at a time and retire pbr once its config is empty.

Anything shunt cannot see is worth knowing about: marks set via `SO_MARK` on a daemon socket (OpenVPN's `--mark`) or by an eBPF program are invisible to any inspection. If a box uses those, check the mark ranges by hand.

<a id="troubleshooting-and-debug-options"></a>
## Troubleshooting & debug options

### Did the policy actually match?

The authoritative check needs no route lookups at all:

```sh
nft reset counters table inet shunt
# generate traffic from the client
nft list chain inet shunt prerouting        # rule counters moved?
tcpdump -ni <policy-interface> host <addr>  # the flow leaves where it should
```

The wire capture is ground truth - but capture a **learned address**, not everything: on a router whose policy interface is also a normal uplink, an unfiltered capture shows traffic that has nothing to do with shunt. An IP echo service is a convenient confirmation, but it only discriminates uplinks that actually have different exits.

The route lookup variant asks the kernel directly:

```sh
ip route get <addr> mark 0x1000000
ip route get <addr>
```

The first answer must name the policy table, the second the normal uplink. With `keep_local` on - the default - pick a destination **outside** your own networks for this: an address in an attached subnet or behind a static route deliberately answers with `main` in both lines, and that is the feature working, not the policy failing.

**The first line needs iproute2's `ip`**, because BusyBox's `route get` does not understand `mark` - one build rejects it outright, the OpenWrt one sends an incomplete netlink request that the kernel answers with `EINVAL`. shunt itself no longer depends on `ip`, so install `ip-tiny` for this check: `route` and `rule` are complete there, the tiny build only strips exotic objects. The second line, without a mark, works with BusyBox too.

Adding `from <client> iif br-lan` makes the lookup more precise, with one further catch worth a confused test session: **`iif` is not optional** there. Without it, `from` a non-local address makes the kernel validate a locally originated lookup and answer `ENETUNREACH` regardless of any table's content, which reads like broken routing and is not.

### What the observer discards, and why

Most DNS answers on a network are of no use to a routing policy, so snoop counts what it discarded and why. `ubus call shunt status` reports those counters, and the LuCI overview shows them with readable labels.

| Verdict | Meaning |
| :--- | :--- |
| qtype | the question was not for an address at all |
| noaddr | an address was asked for, the answer carried none |
| nomatch | the name belongs to no policy |
| dns:E_* | the message did not parse, e.g. `E_RCODE` for NXDOMAIN |
| frame:E_* | the packet did not decode, e.g. `E_FRAG` for a fragment |

`qtype` is usually the largest category and that is expected: current browsers and operating systems ask for **HTTPS records (type 65)** alongside every A and AAAA. `noaddr` is the other half of that distinction: the question *was* A or AAAA, the reply is well formed, and the answer section still holds no address - NODATA.

The checks run in order and the **first** one wins, so these are "first reason to discard" rather than independent counters: a PTR query for a name you route counts as `qtype`, never as `nomatch`.

A high discard count is therefore not a fault. The one number that says whether the observer is doing its job is the matched count next to them.

### When the whole ruleset is gone

`/etc/init.d/firewall stop` runs `fw4 flush`, which deletes **every** nftables table on the box, shunt's among them - `fw4 restart` and `fw4 reload` do not. Nothing is marked from that moment, so all policy traffic takes the normal uplink and a `fallback 'block'` killswitch is not in force either.

The daemon notices this within one `poll_interval`, or immediately if a learned address is written in the meantime, and re-applies its ruleset; the log says so twice, once for the loss and once for the recovery. `nft list table inet shunt` and the LuCI overview both show whether the table is there now, and `ubus call shunt status` counts how often it had to come back:

```sh
ubus call shunt status | grep reapplied
```

A number that keeps climbing means something on the box flushes nftables repeatedly. Look for a service that calls `fw4 flush` or `nft flush ruleset` in an init or hotplug script - shunt only puts its own table back, it cannot stop whatever removes it.

### When a policy stops applying after a reconnect

The kernel removes routes from a policy table when the interface goes down, after which the fwmark rule falls through to `main` while every counter keeps counting. shunt handles this on two levels: a ubus listener on `network.interface` rebuilds the route half on ifup/ifdown, and every poll tick replays the route commands as a keeper. Learned sets survive both. If ubus is unavailable, only the keeper remains, so recovery takes up to one `poll_interval`.

### Debug logging

```sh
uci set shunt.@global[0].debug='1'
uci commit shunt
/etc/init.d/shunt restart
logread -e shunt
```

Set it back to `0` afterwards. Every observed answer and every set write gets a line, which on a busy network is a lot.

<a id="known-limitations"></a>
## Known limitations
These are consequences of the design, stated rather than worked around:

* **Clients that speak DoH or DoT themselves are invisible to snoop.** poll still covers the names you list explicitly; wildcards do not work for those clients. A *resolver* forwarding upstream over DoT or DoH changes nothing.
* **Wildcards require snoop.** poll can only resolve names it was given, and `*.example.com` is not a name.
* **One CDN address serves many domains.** If a policy routes `example.com` and the address behind it also serves a thousand other sites, those sites follow the same policy. This is unsolvable at layer 3 by anything that routes on addresses.
* **The first connection to a newly seen address takes the old path.** snoop learns from the response the client is reading at that moment, so the client's SYN is usually out before the element reaches the set. That connection stays where it started, by design: a flow's route is decided on its first packet and kept on the conntrack entry, so the switch happens at a connection boundary and never mid-stream - see [What shunt creates on the system](#what-shunt-creates-on-the-system). The next connection to the same host starts on the policy interface. Listing the entry point explicitly closes the gap, because poll warms it before any client asks.
* **DNS over TCP is not observed.** Port 53 over TCP needs reassembly, which is out of scope; answers large enough to force TCP are rare in the traffic shunt cares about.
* **Route and rule application is best effort.** At boot a tunnel interface may not exist yet. A rule over an empty table falls through to `main`, so the failure mode is "policy not applied yet", never "traffic broken". Each distinct reason is one warning line.
* **No interface hotplug.** A device that appears later is picked up on the next `ifup` event or within one poll interval, not immediately.
* **tproxy is detected by its module file.** shunt applies a tproxy policy when `nft_tproxy` is loaded or installed under `/lib/modules`. A custom kernel with tproxy built in has neither, and its tproxy policies are skipped as an issue.
* **An outside flush of nftables is repaired, not prevented.** Any tool may delete shunt's table - `fw4 flush` does. shunt re-applies it within one poll interval, or sooner, and until then nothing is marked.

**Out of scope permanently:** resolver-integrated set population (dnsmasq `nftset`, AdGuard Home etc.). Being independent of the DNS backend is the entire point of the project, so adopting a backend-specific mechanism would give up the one property that distinguishes it. Also out: DSCP tagging and user include files.

<a id="support"></a>
## Support
Please report issues with as much detail as possible - the output of `shunt check`, the relevant part of `logread -e shunt` with `debug` enabled, your `/etc/config/shunt`, and the OpenWrt version of the device. Please join the shunt discussion in this [forum thread](https://forum.openwrt.org/t/shunt-policy-based-routing-for-any-dns-backend/252748) or contact me by mail <dev@brenken.org>.

<a id="removal"></a>
## Removal
Stop the service with `/etc/init.d/shunt stop`, which also tears down the nftables table, the routing tables and the ip rules, then remove the `shunt` and `luci-app-shunt` packages if necessary. `shunt flush` does the teardown alone, should anything be left behind.

<a id="donations"></a>
## Donations
You like this project - is there a way to donate? Generally speaking "No" - I have a well-paying full-time job and my OpenWrt projects are just a hobby of mine in my spare time.

If you still insist to donate some bucks ...
* I would be happy if you put your money in kind into other, social projects in your area, e.g. a children's hospice
* Let's meet and invite me for a coffee if you are in my area, the “Markgräfler Land” in southern Germany or in Switzerland (Basel)
* Send your money to my [PayPal account](https://www.paypal.me/DirkBrenken) and I will collect your donations over the year to support various social projects in my area

No matter what you decide - thank you very much for your support!

Have fun!  
Dirk

---

### Overview (`net/simple-captive-portal/README.md`)

# Simple captive portal

This package intercepts/blocks traffic from 'interface' and
redirects http requests to a splash page that you can personalize,
stored in '/etc/simple-captive-portal/'.
After clicking on 'connect' the MAC of the client is allowed,
for 'timeout' seconds (24h), allowing both IPv4 and IPv6.

If your guest interface defaults to input drop or reject (recommended),
make sure to allow tcp 8888-8889 on input (and also dns and dhcp).

Here an example (ipv4) firewall configuration.

```
config zone
	option name 'guest'
	option forward 'REJECT'
	option output 'ACCEPT'
	option input 'REJECT'
	option network 'guest'

config forwarding
	option dest 'wans'
	option src 'guest'

config rule
	option name 'guest-dhcp'
	option src 'guest'
	option family 'ipv4'
	option proto 'udp'
	option dest_port '67'
	option target 'ACCEPT'

config rule
	option name 'guest-dns'
	option src 'guest'
	option family 'ipv4'
	list proto 'tcp'
	list proto 'udp'
	option dest_port '53'
	option target 'ACCEPT'

config rule
	option name 'guest-portal'
	option src 'guest'
	option family 'ipv4'
	list proto 'tcp'
	option dest_port '8888-8889'
	option target 'ACCEPT'
```

To disable simple-captive-portal, just unset/comment 'interface' in the uci config.

---

### Overview (`net/static-neighbor-reports/README.md`)

# static-neighbor-reports
The `static-neighbor-reports` package allows a user to configure static neighbor reports which
are available for clients to be polled in case they support IEEE802.11k. This greatly improves
the wireless experiences in terms of mobility.

Make sure to enable `ieee80211k` for each Virtual Access Point (VAP) neighbor reports should be
installed to.

## Configuring
The uci config name is `static-neighbor-report`. There's currently only the section
type `neighbor`.

### neighbor
The following options are supported for `neighbor` sections:

#### neighbor_report
This is the binary neighbor report element from a foreign AP. It is required for each neighbor.

#### disabled
Values other than `0` disable the neighbor. It won't be installed into hostapd in this case.
If this option is missing, the neighbor is implicitly active.

#### bssid
The BSSID of the foreign AP. This option can usually be omitted, as it's implicitly present in
the first 6 bytes of the binary neighbor report element.

#### ssid
The SSID of the foreign AP. This option can be omitted, in case it matches the SSID used on the local AP.

#### iface
Space separated list of hostapd interfaces the neighbor should be installed to.

## Retrieving neighbor information
To retrieve the neighbor informations of an AP to be isntalled on a foreign AP, make sure the UCI option
`ieee80211k` is set to `1` on the VAP.

Execute `ubus call hostapd.<ifname> rrm_nr_get_own` on the AP. To get a list of all available interfaces,
execute `ubus list`.

The returned information  follows this format:

```json
{
    "value": [
        "<BSSID>",
        "<SSID>",
        "<Neighbor report element>"
    ]
}
```

---

### Overview (`net/swgp-go/README.md`)

# swgp-go
This readme should help you configure swgp-go.

## Configuration
Edit `/etc/swgp-go.json` with your desired configuration (Client, Server or both).

If you're configuring a server, also properly allow that port in your firewall.

Note the pprof feature is disabled in this package. If you try to enable it
regardless, `swgp-go` will show an error on startup.

## Start daemon
Once you have created your config, enable and run swgp-go using its init script:

```
/etc/init.d/swgp-go enable
/etc/init.d/swgp-go start
```

Please refer to the [Project README](https://github.com/database64128/swgp-go) for more configuration info and examples.

---

### Overview (`net/tailscale/README.md`)

# Tailscale
This readme should help you with tailscale client setup.

> [!NOTE]
> By default this package will use nftables. If you wish to use iptables, the config file `/etc/config/tailscale` can be modfied, changing the line `fw_mode 'nftables'` to `fw_mode 'iptables'`. You can then run `/etc/init.d/tailscale restart` to restart tailscale using your chosen method

## First setup

First, enable and run daemon

```
/etc/init.d/tailscale enable
/etc/init.d/tailscale start
```

Then you should use tailscale utility to get a login link for your device.

Run command and finish device registration with the given URL.
```
tailscale up
```

See the [OpenWrt wiki](https://openwrt.org/docs/guide-user/services/vpn/tailscale/start) for more detailed setup instructions

---

### Overview (`net/tcfilter/README.md`)

# tcfilter

A thin UCI front-end for persistent `tc filter ... ingress` rules.

It does **not** model the flower match/action fields. You write that part as
raw `tc` syntax in `option spec`; the package only manages the device, the
`pref` number, enable/disable, persistence across boot, and re-applying the
rules when the network is reconfigured.

Meant for driving hardware tc-flower offload (e.g. the Realtek DSA PIE
offload) where no higher-level config layer exists.

## Config

`/etc/config/tcfilter`:

```
config tcfilter 'global'
	option enabled '1'

config rule
	option label   'Drop-HomePlug-AV (FRITZ!Box)'
	option device  'lan1'
	option enabled '1'
	option pref    '49152'
	option spec    'protocol 0x88e1 flower skip_sw action drop'
```

The shipped default config carries a few such rules with `enabled '0'`
as ready-to-use examples (FRITZ!Box powerline discovery, mDNS) — set
`device` and flip `enabled` to `1`.

`label` is optional and cosmetic — it only tags the log messages and the
LuCI rows.

`spec` is everything that would follow

```
tc filter add dev <device> ingress pref <pref>
```

* `pref` is **required** — it is how the rule is deleted again.
* Use `skip_sw` so a match the hardware cannot offload fails loudly instead
  of silently installing in software.
* One `rule` per `pref` per device.

## Commands

```
/etc/init.d/tcfilter start        # apply all enabled rules
/etc/init.d/tcfilter stop         # remove them
/etc/init.d/tcfilter reload       # stop + start
/etc/init.d/tcfilter show         # tc -s filter show for every configured device
/etc/init.d/tcfilter reapply_dev lan1
```

## Notes / limitations

* ingress / `clsact` only.
* The `clsact` qdisc is added if missing but never removed on stop (other
  users may share it); only the individual filters are deleted.
* Installed `(device, pref)` pairs are tracked in `/var/run/tcfilter.state`
  so a rule removed from the config is still torn down on the next reload.
* Re-apply hooks: `hotplug.d/iface` runs `start` on `ifup`, `hotplug.d/net`
  runs `reapply_dev` on netdev `add`. `start` is idempotent.
* A `procd_add_reload_trigger` reloads the service when the `tcfilter`
  config changes, so LuCI Save & Apply and
  `uci commit tcfilter && reload_config` take effect on their own; a bare
  `uci commit` still needs an explicit `/etc/init.d/tcfilter reload`.
* No dry-run validation — an invalid `spec` is reported via logread only.
* Free-form `spec` is passed to `tc` by word-split (no shell). Anyone who can
  edit the config can install redirect/mirror rules, i.e. tap traffic.

### Hardware packet counters (Realtek rtl930x PIE offload)

Older rtl930x kernels mis-read the per-rule LOG packet counter: with more
than one offloaded flower rule, only the rule whose PIE rule id was
even-aligned reported a working `tc -s` hardware packet count and the
others stayed at 0 (`rtl930x_packet_cntr_read()` assumed the L3-route
counter layout). Dropping / trapping / redirecting was never affected.

Fixed in the kernel driver upstream (openwrt/openwrt#24994); every
offloaded rule now reports its own count. Firmware built before that
patch still shows the old behaviour.

---

### Overview (`net/travelmate/README.md`)

# Travelmate, a wlan connection manager for travel routers

## Table of Contents
* [Description](#description)
* [Quick Start](#quick-start)
* [Main Features](#main-features)
* [Prerequisites](#prerequisites)
* [Installation and Usage](#installation-and-usage)
* [Travelmate CLI interface](#travelmate-cli-interface)
* [Travelmate config options](#travelmate-config-options)
* [Examples](#examples)
* [Best practice and tweaks](#best-practice-and-tweaks)
* [Troubleshooting & debug options](#troubleshooting-and-debug-options)
* [Support](#support)
* [Removal](#removal)
* [Donations](#donations)

<a id="description"></a>
## Description
If you're taking your laptop, tablet, or phone on an upcoming vacation or business trip, you'll want to connect with friends or complete work on the go. But many hotels don't have a secure wireless network setup or limit you to using a single device at a time.

Travelmate lets you use a small "travel router" to connect all of your devices at once while having total control over your own personal wireless network.

Travelmate runs on OpenWrt and provides an "uplink" to the hotel's wireless access point/hotspot. Travelmate then becomes the Access Point (AP) for you and your companions, providing secure access to the internet. It manages all the network settings, firewall settings, connections to a hotel network, etc. and automatically (re)connects to configured APs/hotspots as they become available.

<a id="quick-start"></a>
## Quick Start
For a typical setup these few steps are enough to get travelmate up and running — see the sections below for details:
1. Install the LuCI companion package: `apk update && apk add luci-app-travelmate` (this pulls in the `travelmate` backend as a dependency).
2. Open LuCI under `Services → Travelmate` and run the **Interface Wizard** once. It creates the uplink interface, the firewall assignment and the required network settings.
3. Switch to the **Wireless Stations** tab, scan the radio you want to use as uplink, click `Add Uplink...` for the desired SSID and enter its credentials.
4. Start and verify the service:

```sh
/etc/init.d/travelmate start
/etc/init.d/travelmate status
```

**Please note:** configure your AP on a different radio than the uplink whenever your router has more than one. Sharing a single radio between AP and uplink works, but costs a noticeable amount of throughput.

<a id="main-features"></a>
## Main Features
* Easy setup from the LuCI web interface with **Interface Wizard** and **Wireless Station manager**
* Display a QR code to transfer the wireless credentials to your mobile devices
* Fast uplink connections
* Supports routers with multiple radios in any order
* Supports all kinds of uplinks, including hidden and enterprise uplinks (WEP-based uplinks are no longer supported)
* Continuously checks the existing uplink quality, e.g. for conditional uplink (dis)connections
* Automatically add open uplinks to your wireless config, e.g. hotel captive portals
* Captive portal detection with a 'heartbeat' function to keep the uplink connection up and running
* Captive portal hook for auto-login configured via uci/LuCI, using an external script (see the examples below)
* VPN hook supports 'wireguard' or 'openvpn' client setups to handle VPN (re)connections automatically
* E-mail hook via 'msmtp' sends notification e-mails after every successful uplink connect
* Proactively scan and switch to a higher priority uplink, replacing an existing connection
* Check router subnet vs. uplink subnet, to show conflicts with the router LAN network
* (Optional) Generate a random unicast MAC address for each uplink connection
* (Optional) Evil twin protection by skipping access points with locally-administered (LAA) BSSIDs
* Configurable retry limit per uplink, with optional unlimited retry mode
* NTP time sync before sending e-mails
* procd init and ntp-hotplug support
* Runtime information available via LuCI and via the 'status' init command
* Log status and debug information to syslog
* STA interfaces operate in an "always off" mode, to make sure that the AP is always accessible

<a id="prerequisites"></a>
## Prerequisites
* [OpenWrt](https://openwrt.org), tested/compatible with current stable and latest OpenWrt snapshot
* The `luci-app-travelmate` package ensures these dependencies are present:
  * 'dnsmasq' as dns backend
  * 'ubus iwinfo' for wlan scanning
  * 'curl' for connection checking and all kinds of captive portal magic, e.g. cp detection and auto-logins
  * a 'wpad' variant to support various WPA encrypted networks (WEP-based uplinks are no longer supported!)
* optional: 'wireguard' or 'openvpn' for vpn client connections
* optional: 'msmtp' to send out travelmate related status messages via e-mail

<a id="installation-and-usage"></a>
## Installation and Usage
* Install OpenWrt on your router and set it up to allow wireless connections. Be sure to set a strong password on the wireless channel(s) so that only you and your companions can use it
* Decide which radio you'll use for the travelmate uplink (radio0, radio1, etc.). 2.4GHz allows a longer (more distant) link, 5GHz provides a faster link. Travelmate works on all radios, but for better performance configure the AP on a separate radio from the one you're planning to use as the uplink
* Install both **travelmate** and **luci-app-travelmate**, the application is located in LuCI under the `Services` menu
* You must use the travelmate **Interface Wizard** one time to configure the uplink, firewall and other network settings
* Use the **Wireless Stations** tab to add an uplink station:
  * **Scan** the radio you chose for the uplink
  * Click `Add Uplink...` for the desired SSID. If there are multiples, choose the one with the largest _Strength_
  * Enter the credentials (password, etc.)
  * You should be "on the air" - test by browsing the internet
* You may add additional uplinks for different locations by repeating the previous step
* Happy traveling ...

The buttons at the bottom of the LuCI overview page behave as follows:

| Button               | Description                                                                                                        |
| :------------------- | :----------------------------------------------------------------------------------------------------------------- |
| **Stop**             | Stops the travelmate service. Greyed out while the service is not running                                            |
| **Interface Wizard** | One-time setup of the uplink interface, firewall zone and metric                                                     |
| **Interface Restart** | Restarts the travelmate uplink interface without touching the configuration                                         |
| **AP QR-Codes**      | Displays a QR code per AP to transfer the wireless credentials to your mobile devices                                |
| **Save & Restart**   | Applies pending configuration changes and restarts the service, this also brings a stopped service back up           |

While a run cycle is in progress (status `processing`), all buttons except **Stop** are temporarily locked to avoid overlapping actions. **Stop** always stays available, so an unwanted or long running cycle can be interrupted at any time - e.g. right after the **Interface Wizard** on a fresh setup, when no uplink station has been configured yet.

<a id="travelmate-cli-interface"></a>
## Travelmate CLI interface
* All important travelmate functions are accessible via CLI, too. If you're going to configure travelmate via CLI, edit the config file `/etc/config/travelmate` and enable the service, see the options reference tables below.

```sh
~# /etc/init.d/travelmate
Syntax: /etc/init.d/travelmate [command]

Available commands:
	start           Start the service
	stop            Stop the service
	restart         Restart the service
	reload          Reload configuration files (or restart if service does not implement reload)
	enable          Enable service autostart
	disable         Disable service autostart
	enabled         Check if service is started on boot
	scan            [<radio>|<ifname>] Scan for available nearby uplinks
	setup           [<iface>] [<zone>] [<metric>] Setup the travelmate uplink interface, by default 'trm_wwan' with firewall zone 'wan' and metric '100'
	running         Check if service is running
	status          Service status
	trace           Start with syscall trace
	info            Dump procd service info
```

The `setup` sub-command is the CLI equivalent of the LuCI **Interface Wizard** and only needs to be run once. The `scan` sub-command writes its result to `/var/run/travelmate/travelmate.scan`, sorted by signal quality, and is used by the **Wireless Stations** tab in LuCI as well.

The `status` sub-command prints the current runtime information:

```sh
~# /etc/init.d/travelmate status
::: travelmate runtime information
  + travelmate_status  : connected, net ok/100
  + frontend_ver       : 2.4.7-1
  + backend_ver        : 2.4.7-1
  + station_id         : radio0/GlutenfreiVerbunden/-
  + station_mac        : 42:40:45:EC:B3:D1
  + station_interfaces : wwan, -
  + station_subnet     : 10.168.20.0 (lan: 10.200.1.0)
  + run_flags          : autoadd: ✘, captive: ✔, eviltwin: ✘, mail: ✔, netcheck: ✘, ntp: ✔, proactive: ✔, randomize: ✔, vpn: ✘
  + last_run           : mode: start, date / time: 2026-08-06 09:08:24, memory: 412.35 MB available
  + system_info        : cores: 2, fetch: curl, Cudy TR3000 v1, mediatek/filogic, OpenWrt SNAPSHOT (r32287-1c7ec8ab19)
```

The `travelmate_status` field reports one of the following states:

| State           | Description                                                                                                     |
| :-------------- | :-------------------------------------------------------------------------------------------------------------- |
| `connected`     | An uplink is connected, followed by the connection details, e.g. `connected, net ok/100`                          |
| `processing`    | A run cycle is currently in progress, e.g. scanning the radios or waiting for an uplink to come up                |
| `not connected` | No uplink is connected and no run cycle is active, i.e. travelmate is idle and waiting for the next trigger       |
| `program error` | A fatal error occurred, the daemon has stopped, please check the syslog                                           |

A stopped service does not report a state at all, it truncates the runtime file instead - LuCI shows this as `stopped`.

<a id="travelmate-config-options"></a>
## Travelmate config options
* Usually the pre-configured travelmate setup works quite well and no manual config overrides are needed. All options listed below apply to the 'global' section:

| Option             | Default                            | Description/Valid Values                                                                              |
| :----------------- | :--------------------------------- | :---------------------------------------------------------------------------------------------------- |
| trm_enabled        | 0, disabled                        | set to 1 to enable the travelmate service (this will be done by the Interface Wizard as well!)        |
| trm_debug          | 0, disabled                        | set to 1 to get the full debug output (logread -e "trm-")                                             |
| trm_iface          | -, not set                         | uplink- and procd trigger network interface, configured by the 'Interface Wizard'                     |
| trm_laniface       | -, lan                             | logical LAN network interface, default is 'lan'                                                       |
| trm_radio          | -, not set                         | restrict travelmate to certain radio(s)                                                               |
| trm_revradio       | 0, disabled                        | change the radio processing order, e.g. 'radio1 radio0'                                               |
| trm_captive        | 1, enabled                         | check the internet availability and handle captive portal redirections                                |
| trm_netcheck       | 0, disabled                        | treat missing internet availability as an error, see the note below                                   |
| trm_proactive      | 0, disabled                        | proactively scan and switch to a higher prioritized uplink, despite of an already existing connection |
| trm_autoadd        | 0, disabled                        | automatically add open uplinks like hotel captive portals to your wireless config                     |
| trm_ssidfilter     | -, not set                         | list of SSID patterns for filtering/skipping specific open uplinks, e.g. 'Chromecast*'                |
| trm_randomize      | 0, disabled                        | generate a random unicast MAC address for each uplink connection                                      |
| trm_eviltwin       | 0, disabled                        | detect and skip access points with locally administered (LAA) BSSIDs to mitigate evil twin attacks    |
| trm_triggerdelay   | 2                                  | additional trigger delay in seconds before travelmate processing begins                               |
| trm_maxretry       | 3                                  | retry limit to connect to an uplink, set to '0' for unlimited retries                                 |
| trm_minquality     | 35                                 | minimum signal quality threshold as percent for conditional uplink (dis-) connections                 |
| trm_maxwait        | 30                                 | how long should travelmate wait for a successful wlan uplink connection                               |
| trm_timeout        | 60                                 | overall retry timeout in seconds                                                                      |
| trm_maxautoadd     | 5                                  | limit the max. number of automatically added open uplinks. To disable this limitation set it to '0'   |
| trm_captiveurl     | http://detectportal.firefox.com    | custom/pre-configured provider URLs that will be used for connectivity- and captive portal checks     |
| trm_useragent      | Mozilla/5.0 ...                    | pre-configured user agents that will be used for connectivity- and captive portal checks              |
| trm_nice           | 0, normal priority                 | change the priority of the travelmate background processing                                           |
| trm_mail           | 0, disabled                        | sends notification e-mails after every succesful uplink connect                                       |
| trm_mailreceiver   | -, not set                         | e-mail receiver address for travelmate notifications                                                  |
| trm_mailsender     | no-reply@travelmate                | e-mail sender address for travelmate notifications                                                    |
| trm_mailtopic      | travelmate connection to '<sta>'   | topic for travelmate notification E-Mails                                                             |
| trm_mailprofile    | trm_notify                         | profile used by 'msmtp' for travelmate notification E-Mails                                           |
| trm_vpn            | 0, disabled                        | VPN connections will be managed by travelmate                                                         |
| trm_stdvpnservice  | -, not set                         | standard vpn service which will be automatically added to new STA profiles                            |
| trm_stdvpniface    | -, not set                         | standard vpn interface which will be automatically added to new STA profiles                          |

**Please note:** 'trm_netcheck' is evaluated independently of 'trm_captive'. A detected captive portal does not count as an error as long as 'trm_captive' is enabled - such an uplink stays connected so that a manual or script based portal login is still possible. With 'trm_captive' disabled travelmate can neither allowlist the portal domain nor run a login script, so a detected portal is treated like a failed connectivity check. Only a failed connectivity check counts, and it is confirmed by a second probe before it is acted on. Such a failure is then handled like a failed connection attempt: after 'trm_maxretry' tries the affected uplink gets disabled in the travelmate config. A longer lasting outage of your upstream provider may therefore disable all configured uplinks, which have to be re-enabled manually.

* Per uplink there is an additional 'uplink' section in the travelmate config, with the following options:

| Option             | Default                            | Description/Valid Values                                                                              |
| :----------------- | :--------------------------------- | :---------------------------------------------------------------------------------------------------- |
| enabled            | 1, enabled                         | enable or disable the uplink, automatically set if the retry limit was reached                        |
| revive             | 0, disabled                        | re-enable this uplink after n run cycles if the retry limit disabled it, minimum '10'                 |
| device             | -, not set                         | match the 'device' in the wireless config section                                                     |
| ssid               | -, not set                         | match the 'ssid' in the wireless config section                                                       |
| bssid              | -, not set                         | match the 'bssid' in the wireless config section                                                      |
| script             | -, not set                         | reference to an external auto login script for captive portals                                        |
| script_args        | -, not set                         | optional runtime args for the auto login script                                                       |
| macaddr            | -, not set                         | use a specified MAC address for the uplink                                                            |
| vpn                | 0, disabled                        | automatically handle VPN (re-) connections                                                            |
| vpnservice         | -, not set                         | reference the already configured 'wireguard' or 'openvpn' client instance as vpn provider             |
| vpniface           | -, not set                         | the logical vpn interface, e.g. 'wg0' or 'tun0'                                                       |

<a id="examples"></a>
## Examples
**VPN client setup**  
Please read one of the following guides to get a working vpn client setup on your travel router:

* [Wireguard client setup guide](https://openwrt.org/docs/guide-user/services/vpn/wireguard/client)
* [OpenVPN client setup guide](https://openwrt.org/docs/guide-user/services/vpn/openvpn/client-luci)

Make sure to uncheck the "Bring up on boot" option during vpn interface setup, so that netifd doesn't interfere with travelmate. Also prevent potential vpn protocol autostarts, e.g. add an additional 'globals' section in newer openvpn uci configs:

```
config globals 'globals'
        option autostart '0'
```

Once your vpn client connection setup is correct, you can reference that config in travelmate to handle VPN (re-) connections automatically.

**E-Mail setup**  
To use e-mail notifications you have to set up the package 'msmtp'. Modify the file `/etc/msmtprc`, e.g. for gmail:

```
[...]
defaults
auth            on
tls             on
tls_certcheck   off
timeout         5
syslog          LOG_MAIL
[...]
account         trm_notify
host            smtp.gmail.com
port            587
from            xxx@gmail.com
user            yyy
password        zzz
```

Finally enable e-mail support in travelmate and add a valid e-mail receiver address.

**Captive portal auto-logins**  
For automated captive portal logins you can reference an external shell script per uplink. All login scripts have to be executable and located in `/etc/travelmate` with the extension `.login`. A login script signals its result via the exit code: `0` means the login succeeded, any other value means it failed. Only `0` makes travelmate re-check the connectivity right away, every other value is just logged. The package ships multiple ready to run auto-login scripts:

* 'wifibahn.login' for german DB railway hotspots
* 'telekom.login' for telekom hotspots (DE)
* 'vodafone.login' for vodafone hotspots (DE)
* 'generic-user-pass.login' a template to demonstrate the optional parameter handling in login scripts

A typical and successful captive portal login looks like this:

```
[...]
user.info trm-2.4.7-1[26222]: captive portal domain 'www.wifibahn.de' added to dhcp rebind allowlist
user.info trm-2.4.7-1[26222]: captive portal login script for 'www.wifibahn.de' has been finished with rc '0'
user.info trm-2.4.7-1[26222]: connected to uplink 'radio1/WIFI@DB/-' with mac 'B2:9D:F5:96:86:A4' (1/3)
[...]
```
**Building your own login script**  
The fastest way to a working script is to record the login once by hand and then replay it with curl. Any browser's developer tools can do the recording:

1. Connect a client to the hotspot - either directly, or through travelmate's own AP while the uplink is up - and open the portal page.
2. Open the developer tools (usually `F12`) and switch to the `Network` tab. Enable `Preserve log` (Chromium, Edge, Safari) resp. `Persist Logs` (Firefox, behind the gear icon) and `Disable cache`. A portal login almost always ends in a redirect, and without these options the recorded entries are dropped at that point.
3. Perform the login manually and watch which requests are sent.
4. Right-click the request that carries your credentials and choose `Copy` -> `Copy as cURL`. You now have the exact URL, method, headers, cookies and form fields as the browser sent them. `Save All As HAR` resp. `Export HAR` records the whole session if you want to study it later - note that recent Chromium versions strip cookies and authorization headers from the export unless you allow sensitive data in the devtools settings.
5. Strip the copied command down: the browser adds a lot of `Accept*`, `Sec-*` and `Priority` headers that no portal cares about. Keep the request body, the `Content-Type` and whatever the portal actually validates, then replace curl's flags with travelmate's `${trm_fetchcmd} ${trm_fetchparm}` and `--user-agent "${trm_useragent}"`.
6. Look for values that are only valid for one session - CSRF tokens, session ids, `sid` parameters. Those must not be copied into the script but fetched at runtime, see `wifibahn.login` (cookie jar plus awk) or `vodafone.login` (json response plus jsonfilter) for the two usual patterns.
7. Never hardcode credentials. Pass them via the uplink's `script_args` option and read them as `${1}` and `${2}`, like `generic-user-pass.login` does.
8. Test the script on the router while the portal is actually in the way: `sh -x /etc/travelmate/my.login user pass; echo "rc: ${?}"`. Make sure it only exits `0` when the login really succeeded - a script that reports success too eagerly is worse than one that fails, because travelmate will happily keep the uplink.

The portal domain travelmate detected is in the system log: `logread -e "trm-"` shows it as `captive portal domain '<domain>' added to dhcp rebind allowlist`.

Hopefully more scripts for different captive portals will be provided by the community!

<a id="best-practice-and-tweaks"></a>
## Best practice and tweaks
**Radio assignment**  
If your router has more than one radio, keep the AP and the uplink on separate radios - sharing one radio between both costs a noticeable amount of throughput. Use `trm_radio` to restrict travelmate to the uplink radio(s) and `trm_revradio` to change the processing order, e.g. to prefer 5GHz over 2.4GHz.

**Signal quality thresholds**  
`trm_minquality` (default 35%) decides twice: a scan result below the threshold is not considered as a connection candidate at all, and an existing connection that drops below it is torn down so that travelmate can look for something better. Raising the value makes travelmate switch away earlier, lowering it keeps weak uplinks alive longer. Values well above 50% tend to make travelmate restless in hotels.

**Retry behaviour**  
`trm_maxretry` (default 3) limits the connection attempts per uplink. When the limit is reached, the affected uplink is disabled in the travelmate config and has to be re-enabled manually - which is intentional for permanently broken credentials, but worth keeping in mind in combination with `trm_netcheck`. Set `trm_maxretry` to '0' for unlimited retries if you never want an uplink to be disabled automatically.

Per uplink, `revive` softens that. With a value of n, an uplink disabled by the retry limit is set back to 'enabled' after n run cycles, so a network that was only temporarily unavailable comes back on its own. Values below 10 are raised to 10, since a run cycle is roughly `trm_timeout` seconds long and shorter intervals only produce needless reconnect attempts. The number of revivals is capped by the global `trm_maxretry`, which also means that `revive` has no effect with `trm_maxretry` set to '0'. The cycle and round counters are kept in the runtime file '/var/run/travelmate/travelmate.revive', they are never written to the config and are cleared when the service is stopped. The enable and disable itself is still recorded in the travelmate config, so the LuCI station list always shows the real state.

**Open uplinks**  
`trm_autoadd` adds open networks to your wireless config on the fly, which is handy in hotels and on trains. Keep `trm_maxautoadd` at a sane value so that a busy location doesn't flood your config, and use `trm_ssidfilter` to skip the usual noise, e.g. `Chromecast*` or printer and camera SSIDs.

**Privacy and evil twin protection**  
`trm_randomize` generates a new random unicast MAC address for each uplink connection, so a hotspot operator can't trivially recognize your router across visits. `trm_eviltwin` skips access points with a locally administered (LAA) BSSID, which is a cheap indicator for a spoofed access point - note that some legit setups (mesh, repeaters) use LAA BSSIDs as well.

**Timing**  
`trm_maxwait` (default 30s) is the budget for a single connection attempt and also scales the internal curl timeouts. On slow uplinks or with captive portals that take their time, raising it to 45-60s helps, lowering it makes travelmate give up on weak candidates faster. `trm_triggerdelay` adds a delay before processing starts after an ifup event, which is useful if your uplink needs a moment to settle.

**Subnet conflicts**  
Travelmate compares the uplink subnet against your router LAN network and logs a warning if both overlap. If you run into this frequently, change your LAN network to something uncommon, e.g. `10.200.1.0/24`, because most hotel networks use `192.168.0.0/24` or `192.168.1.0/24`.

<a id="troubleshooting-and-debug-options"></a>
## Troubleshooting & debug options
Travelmate provides an optional debug mode that writes detailed diagnostic information about every processing step to the system log. Under normal conditions only the relevant status messages are logged, to keep regular runs clean and silent. To enable debug mode, set the option `trm_debug` to `1`.

Whenever you encounter travelmate related processing problems, please enable `trm_debug`, restart travelmate and check the `Log View` tab in LuCI (or the syslog via `logread -e "trm-"`).

Typical symptoms:
* No uplink is found although it is in range: compare the reported signal quality in the debug log against `trm_minquality`, and make sure the uplink is still enabled in the travelmate config - the retry limit may have disabled it
* An uplink connects but has no internet: the debug output of `f_net` shows the probe host, the effective url and the resulting state (`net ok`, `net cp '<domain>'` or `net nok`). A `net cp` state means a captive portal was detected and is waiting for a login
* A captive portal login script doesn't run: it has to be executable and located in `/etc/travelmate` with the extension `.login`, everything else is rejected for security reasons
* The connection drops right after connecting: check for a subnet conflict warning between the uplink network and your router LAN network

<a id="support"></a>
## Support
Please join the travelmate discussion in this [forum thread](https://forum.openwrt.org/t/travelmate-support-thread/5155) or contact me by mail <dev@brenken.org>
If you want to report an error, please describe it in as much detail as possible - with (debug) logs, the current travelmate status and your travelmate configuration.

<a id="removal"></a>
## Removal
Stop the travelmate daemon with _/etc/init.d/travelmate stop_ and remove the travelmate packages if necessary.

<a id="donations"></a>
## Donations
You like this project - is there a way to donate? Generally speaking "No" - I have a well-paying full-time job and my OpenWrt projects are just a hobby of mine in my spare time.

If you still insist to donate some bucks ...
* I would be happy if you put your money in kind into other, social projects in your area, e.g. a children's hospice
* Let's meet and invite me for a coffee if you are in my area, the “Markgräfler Land” in southern Germany or in Switzerland (Basel)
* Send your money to my [PayPal account](https://www.paypal.me/DirkBrenken) and I will collect your donations over the year to support various social projects in my area

No matter what you decide - thank you very much for your support!

Have fun!  
Dirk

---

### Overview (`net/uneighbord/README.md`)

# uneighbord

uneighbord synchronizes hostapd 802.11k Neighbor Reports between local
`hostapd.*` BSSes and other OpenWrt access points on the same LAN, over
IPv6 link-local UDP multicast. Reports are only shared between BSSes with
the same SSID, and installed through hostapd's ubus API.

## Requirements

`ucode`, `ucode-mod-ubus`, `ucode-mod-uloop`, `ucode-mod-socket`, `ucode-mod-uci`, `ucode-mod-log`.

## Build and install

```sh
make menuconfig                  # select Network -> uneighbord
make package/uneighbord/compile V=s
```

Install the resulting package, then:

```sh
/etc/init.d/uneighbord enable
/etc/init.d/uneighbord start
```

## Configuration

```uci
config uneighbord 'main'
        option enabled '1'
        option network 'lan'
```

`network` is an OpenWrt logical network name (not a device name); its
`l3_device` is used for multicast membership and scope.

## How it works

Every 30 seconds each AP announces its local Neighbor Reports (from
`rrm_nr_get_own`) as a small JSON packet to a fixed IPv6 multicast group on
port 32027. For each local BSS, same-SSID reports from other local BSSes
and remote peers are merged, excluding the BSS's own entry, and installed
via `rrm_nr_set`. Remote peers are dropped after 90 seconds without a
fresh announcement.

uneighbord assumes a trusted L2/backhaul network: announcements are
unauthenticated, so any host able to reach the multicast group can
influence the installed Neighbor Report list.

## Debugging

```sh
ubus list 'hostapd.*'
ubus call hostapd.<interface> rrm_nr_list
logread -e uneighbord
ss -6 -u -a -n | grep 32027
ip -6 maddr show dev br-lan
```

---

### Overview (`net/wg-installer/README.md`)

## Wireguard Installer

This tool can be used to automatically create wireguard tunnels. Using rpcd a new wireguard interface is created on the server where the client can connect to.

## Installation

For Server

    opkg install wireguard-installer-server

For Client

    opkg install wireguard-installer-client

Wiregurad server automatically installs a user and associated ACL to use the wireguard-installer-server features.
The user is called wginstaller and so is the password.

## Usage

Get Usage Statistics

    wg-client-installer get_usage --ip 127.0.0.1 --user wginstaller --password wginstaller

Register Tunnel Interface

    wg-client-installer register --ip 127.0.0.1 --user wginstaller --password wginstaller --bandwidth 10

## Hotplugs

- wg-installer-server-hotplug-babeld: mesh automatically via wireguard with babeld

---

### Overview (`net/wgsd/README.md`)

## Wireguard Service Discovery (Mesh)

This tool allows you to build a mesh from wireguard tunnels and also traversal NAT.

See this article: https://www.jordanwhited.com/posts/wireguard-endpoint-discovery-nat-traversal/
Also see project on github: https://github.com/jwhited/wgsd


## Usage

On server edit /etc/Corefile to select on which port DNS-SD should be serving and also which interface to use.

Here is the example, where port `5353` will be used by coredns, `coredns.lan.` zone be used for discovery and `vpn_wg` interface be used to gather peers information.

    .:5353 {
      log
      #whoami
      wgsd coredns.lan. vpn_wg
    }

On a client you should put to the cron line like that:

    1,6,11,16,21,26,31,36,41,46,51,56 * * * * /usr/bin/wgsd-client -device vpn_wg -dns your.central.node:5353 -zone coredns.lan


### Note

All peers that should connect to each other should know other peers.
So you should setup your central node as a first peer followed with peers.
E.g. let's say we have a Cloud-Router (CR), Alice and the Bob. Then you should configure peers for CR (with the address) and Bob on Alice's side and CR and Alice on Bob's.

---

### Overview (`net/wifischedule/README.md`)

# wifischedule
Turns WiFi on and off according to a schedule on an openwrt router

## Components
* wifischedule: Shell script that creates cron jobs based on configuration provided in UCI and does all the other logic of enabling and disabling wifi with the use of `/sbin/wifi` and `/usr/bin/iwinfo`. Can be used standalone.
* luci-app-wifischedule: LUCI frontend for creating the UCI configuration and triggering the actions. Depends on wifischedule.


## Use cases
You can create user-defined events when to enable or disable WiFi. 
There are various use cases why you would like to do so:

1. Reduce power consumption and therefore reduce CO2 emissions.
2. Reduce emitted electromagnatic radiation.
3. Force busincess hours when WiFi is available.

Regarding 1: Please note, that you need to unload the wireless driver modules in order to get the most effect of saving power.
In my test scenario only disabling WiFi saves about ~0.4 Watt, unloading the modules removes another ~0.4 Watt.

Regarding 2: Think of a wireless accesspoint e.g. in your bedrom, kids room where you want to remove the ammount of radiation emitted.

Regarding 3: E.g. in a company, why would wireless need to be enabled weekends if no one is there working? 
Or think of an accesspoint in your kids room when you want the youngsters to sleep after 10 pm instead of facebooking...

## Configuration
You can create an arbitrary number of schedule events. Please note that there is on sanity check done wheather the start / stop times overlap or make sense.
If start and stop time are equal, this leads to disabling the WiFi at the given time.

Logging if enabled is done to the file `/var/log/wifi_schedule.log` and can be reviewed through the "View Logfile" tab.
The cron jobs created can be reviewed through the "View Cron Jobs" tab.

Please note that the "Unload Modules" function is currently considered as experimental. You can manually add / remove modules in the text field.
The button "Determine Modules Automatically" tries to make a best guess determining regarding the driver module and its dependencies.
When un-/loading the modules, there is a certain number of retries (`module_load`) performed.

The option "Force disabling wifi even if stations associated" does what it says - when activated it simply shuts down WiFi.
When unchecked, its checked every `recheck_interval` minutes if there are still stations associated. Once the stations disconnect, WiFi is disabled. To ignore associated stations add their MAC to `ignore_stations`.

Please note, that the parameters `module_load`, `recheck_interval` and `ignore_stations` are only accessible through uci.

## UCI Configuration `wifi_schedule`
UCI configuration file: `/etc/config/wifi_schedule`:

```
config global
        option logging '0'
        option enabled '0'
        option recheck_interval '10'
        option modules_retries '10'
#       option ignore_stations 'AA:AA:AA:AA:AA:AA BB:BB:BB:BB:BB:BB'

config entry 'Businesshours'
        option enabled '0'
        option daysofweek 'Monday Tuesday Wednesday Thursday Friday'
        option starttime '06:00'
        option stoptime '22:00'
        option forcewifidown '0'

config entry 'Weekend'
        option enabled '0'
        option daysofweek 'Saturday Sunday'
        option starttime '00:00'
        option stoptime '00:00'
        option forcewifidown '1'
```

## Script: `wifi_schedule.sh`
This is the script that does the work. Make your changes to the UCI config file: `/etc/config/wifi_schedule`

Then call the script as follows in order to get the necessary cron jobs created:

`wifi_schedule.sh cron`

All commands:
 
```
wifi_schedule.sh cron|start|startup|stop|forcestop|recheck|getmodules|savemodules|help

    cron: Create cronjob entries.
    start: Start wifi.
    startup: Checks current timewindow and enables/disables WIFI accordingly.
    stop: Stop wifi gracefully, i.e. check if there are stations associated and if so keep retrying.
    forcestop: Stop wifi immediately.
    recheck: Recheck if wifi can be disabled now.
    getmodules: Returns a list of modules used by the wireless driver(s)
    savemodules: Saves a list of automatic determined modules to UCI
    help: This description.
```

## Startup Script: `/etc/init.d/wifi_schedule`
Makes sure time window is checked and WIFI is enabled or disabled accordingly when powering on the router.

---

### Overview (`net/xl2tpd/README.md`)

# OpenWrt Package for xl2tpd

xl2tpd is a development from the original l2tpd package originally written by
Mark Spencer, subsequently forked by Scott Balmos and David Stipp, inherited
by Jeff McAdams, modified substantially by Jacco de Leeuw and then forked
again by Xelerance (after it was abandoned by l2tpd.org).

## Rationale for inclusion in OpenWrt

l2tpd has some serious alignment problems on RISC platforms. It also runs
purely in userspace.

Some of the features added in this fork include:

1. IPSec SA reference tracking inconjunction with openswan's IPSec transport
   mode, which adds support for multiple clients behind the same NAT router
   and multiple clients on the same internal IP behind different NAT routers.

2. Support for the pppol2tp kernel mode L2TP.

3. Alignment and endian problems resolved.

hcg

## UCI options

`server` takes the form `host[:port]` with port defaults to `1701`.  It
specifies the l2tp server's address.

`checkup_interval` tells netifd to check after that many seconds since last
setup attempt to see if the interface is up.  If not it should issue another
teardown/setup round to retry the negotiation.  This option defaults to 0 and
netifd will not do the check and retry.

The following are generic ppp options and should have the same format and
semantics as with other ppp-related protocols.  See
[uci/network#protocol_ppp](https://openwrt.org/docs/guide-user/network/wan/wan_interface_protocols#protocol_ppp_ppp_over_modem)
for details.

	username
	password
	keepalive
	ipv6
	mtu
	pppd_options

---

### Overview (`utils/cache-domains/README.md`)

# cache-domains

hotplug script to dynamically configure the local DNS (dnsmasq) to redirect game content servers to a LAN cache. Definitive list dynamically obtained from https://github.com/uklans/cache-domains.

## Configuration
The configuration file (`/etc/cache-domains.json`) follows the same [syntax as the upsteam file](https://github.com/uklans/cache-domains/blob/master/scripts/config.example.json). The key for each `cache_domains` member matches the name of one of the `.txt` files in the [upstream root directory](https://github.com/uklans/cache-domains/blob/master/), except for the `default` key which matches the all the unreferenced `.txt` files. The value of each `cache_domains` member maps to one of the keys of the `ips` members, Thus mapping a cached domain to a list of IP addresses/LAN cache server.

```json
{
	"ips": {
		"server1":	["10.10.3.10", "10.10.3.11"],
		"server2":	"10.10.3.12",
		"server3":	"10.10.3.13"
	},
	"cache_domains": {
		"default":	"server1",
		"blizzard":	"server1",
		"origin":	"server1",
		"steam":	"server2",
		"wsus":		"server3",
		"xboxlive":	"server3"
	}
}
```

## Configure/Cleanup
`/usr/bin/cache-domains configure` will configure the local DNS (dnsmasq) to redirect the configured cache domains. `/usr/bin/cache-domains cleanup` will cleanup redirection. The hotplug script calls `/usr/bin/cache-domains configure` when the WAN interface is brought up.

## Testing
After configuring with the above example configuration, running `nslookup lancache.steamcontent.com` would return `10.10.3.12`

---

### Overview (`utils/pservice/README.md`)

# uci

`disabled`, bool, default `0`

`name`, string, name of the service instance

`command`, file, the service instance executable

`args`, list of args

`stderr`, bool, default `0`, log stderr output of the service instance

`stdout`, bool, default `0`, log stdout output of the service instance

`env`, list of environment variable settings of the form `var=val`

`file`, list of file names.  Service instances will be restarted if content of
these files have changed on service reload event.

`respawn_threshold`, uinteger, default `3600`, time in seconds the instances
have to be in running state to be considered a valid run

`respawn_timeout`, uinteger, default `5`, time in seconds the instance should
be delayed to start again after the last crash

`respawn_maxfail`, uinteger, default `5`, maximum times the instances can
crash/fail in a row and procd will not try to bring it up again after this
limit has been reached

# notes and faq

Initial environment variables presented to service instances may be different
from what was observed on the interactive terminal.  E.g. `HOME=/` may affect
reading `~/.ssh/known_hosts` of dropbear ssh instance.

	PATH=/usr/sbin:/usr/bin:/sbin:/bin PWD=/ HOME=/

If `list args xxx` seems to be too long causing pain, consider using `/bin/sh`
as the `command`.  It is also worth noting that uci supports multi-line option
value.

Child processes will keep running when their parent process was killed.  This
is especially the case and should be taken into account with option `command`
being `/bin/sh` and it is recommended to use `exec` as the last shell command.

---

### Overview (`lang/python/python3-packages/README.md`)

This package allows users to package python3 modules without creating package
Makefiles for each individual module and their dependencies.  It provides a
way making packaging python3 packages faster and may also facilitate the process
of developing Makefiles for new python3 packages

This is a raw DEVEL only package.  Using it may entails a lot of implementation
details and you may need to resolve target dependencies and package details on
your own

- Third party python3 packages may depend on features not included in e.g.
  python3-light
- Some python3 modules may require host install of another module to progress,
  e.g. target cryptography requires host cffi
- Some python3 modules have external C library dependencies, e.g. pyOpenSSL
  requires openssl libs
- Some packages may have an autoconf configure script whose arguments we
  cannot control with pip and has to be passed on (hacked) by overriding some
  environment variables

## How it works

1. Install host modules required for building target modules
2. Install each target module to separate directories
3. Install another copy of modules for cleanup purposes to make list of
   installed files to be removed from target modules installed in step 2

Why should it be so

1. Installing target cryptography requires host installation of cffi module
2. cryptography requires setuptools and pip will install its own copy with
   --ignore-installed.  When PACKAGE_python3-setuptools is also selected, opkg
   will complain of data file clashes if it was not removed here.

Pip will handle dependency requirements of python3 modules, but external
dependencies like c libraries has to be prepared by the build system.  The
issue is that there is currently no way to express such dependencies, thus may
cause build failure, e.g. pycrypto requires the presence of libgmp to build
successfully.

## Tips

If something goes wrong, we can add additional arguments to pip command
line to check the detailed build process.  Some useful arguments may be

- -v, for verbose output.  Repeat this option if the current level of
  verbosity is not enough
- --no-clean, for preserving pip build dir on build failure

## Examples

tornado (python-only module)

	CONFIG_PACKAGE_python3-packages=y
	CONFIG_PACKAGE_python3-packages-list="tornado==6.0.2"

cryptography (requires installation of host modules and cleanup on target modules)

	CONFIG_PACKAGE_python3-packages=y
	CONFIG_PACKAGE_python3-packages-list-host="cffi"
	CONFIG_PACKAGE_python3-packages-list="cryptography"
	CONFIG_PACKAGE_python3-packages-list-cleanup="setuptools"

pycrypto 2.7a1 (python module with autoconf configure script; depends on
libgmp; broken wmmintrin.h).  2.6.1 does not work because of a flaw in
the setup.py hardcoding host include directory

	CONFIG_PACKAGE_libgmp=y
	CONFIG_PACKAGE_python3-packages=y
	CONFIG_PACKAGE_python3-packages-list="https://github.com/dlitz/pycrypto/archive/v2.7a1.tar.gz"
	CONFIG_PACKAGE_python3-packages-envs="ac_cv_header_wmmintrin_h=no build_alias=$(GNU_HOST_NAME) host_alias=$(GNU_TARGET_NAME) target_alias=$(GNU_TARGET_NAME)"
	CONFIG_PACKAGE_python3-packages-extra-deps="libgmp.so.10"

---

### Overview (`net/jool/files/readme.md`)

# [Jool](https://nicmx.github.io/Jool/en/index.html)

## Documentation

[See here](https://nicmx.github.io/Jool/en/documentation.html).

You might also want to see [contact info](https://nicmx.github.io/Jool/en/contact.html).

## Usage

### Start script

This package includes a start script that will:

  1. Read the configuration file `/etc/config/jool`
  2. Determine what services are active
  3. Run `jool` with procd

### For now this means that
  
- The services will be disabled by default in the uci config `(/etc/config/jool)`
- The only uci configuration support available for the package is to enable or disable each instance or the entire deamon
- There is no uci support and configuration will be saved at `/etc/jool/`
- Only one instance of jool(nat64) can run with the boot script
- Only one instance of jool(siit) can run with the boot script
- For now there is no way of overriding of the configuration file's paths

The configuration files the startup script uses for each jool instance are:

- jool(nat64): `/etc/jool/jool-nat64.conf.json`
- jool(siit): `/etc/jool/jool-siit.conf.json`

### OpenWrt tutorial

For a more detailed tutorial refer to this [wiki page](https://openwrt.org/docs/guide-user/network/ipv6/nat64).

---

### Overview (`net/nebula/files/README.md`)

# README

README is available at [https://docs.openwrt.melmac.net/nebula/](https://docs.openwrt.melmac.net/nebula/).

---

### Overview (`net/speedtest-netperf/files/README.md`)

Network Performance Testing
===========================

## Introduction

The `speedtest-netperf` package provides a convenient means of on-device network performance testing for OpenWrt routers. Such performance testing primarily includes characterizing the network throughput and latency, but CPU usage can also be an important secondary measurement. These aspects of network testing are motivated chiefly by the following:

1. **Throughput:** Network speed measurements can help troubleshoot transfer problems, and be used to determine the truth of an ISP's promised speed claims. Accurate throughput numbers also provide guidance for configuring other software's settings, such as SQM ingress/egress rates, or bandwidth limits for Bittorrent.

2. **Latency:** Network latency is a key factor in high-quality experiences with real-time or interactive applications such as VOIP, gaming, or video conferencing, and excessive latency can lead to undesirable dropouts, freezes and lag. Such latency problems are endemic on the Internet and often the result of [bufferbloat](https://www.bufferbloat.net/projects/). Systematic latency measurements are an important part of identifying and mitigating this bufferbloat.

3. **CPU Usage:**  Observing CPU usage under network load gives insight into whether the router is CPU-bound, or if there is CPU "headroom" to support even higher network throughput. In addition to managing network traffic, a router actively running a speed test will also use CPU cycles to generate network load, and measuring this distinct CPU usage also helps gauge its impact.

**Note:** _The `speedtest-netperf.sh` script uses servers and network bandwidth that are provided by generous volunteers. Feel free to use the script to test your SQM configuration or troubleshoot network and latency problems, but be warned that continuous or high-rate use of the servers may result in denied access. Happy testing!_


## Theory of Operation

When launched, `speedtest-netperf.sh` uses the local `netperf` application to run several upload and download streams (files) to a server on the Internet. This places a heavy load on the bottleneck link of your network (probably your connection to the Internet) while measuring the total bandwidth of the link during the transfers. Under this network load, the script simultaneously measures the latency of pings to see whether the file transfers affect the responsiveness of your network. Additionally, the script tracks the per-CPU processor usage, as well as the CPU usage of the `netperf` instances used for the test. On systems that report CPU frequency scaling, the script can also report per-CPU frequencies.

The script operates in two distict modes for network loading: *sequential* and *concurrent*. In the default sequential mode, the script emulates a web-based speed test by first downloading and then uploading network streams. In concurrent mode, the script mimics the stress test of the [FLENT](https://github.com/tohojo/flent) program by dowloading and uploading streams simultaneously.

Sequential mode is preferred when measuring peak upload and download speeds for SQM configuration or testing ISP speed claims, because the measurements are unimpacted by traffic in the opposite direction.

Concurrent mode places greater stress on the network, and can expose additional latency problems. It provides a more realistic estimate of expected bidirectional throughput. However, the download and upload speeds reported may be considerably lower than your line's rated speed. This is not a bug, nor is it a problem with your internet connection. It's because the ACK (acknowledge) messages sent back to the sender may consume a significant fraction of a link's capacity (as much as 50% with highly asymmetric links, e.g 15:1 or 20:1).

The script also supports measuring latency when idle as a baseline prior to measuring under network load. This allows better determination of induced latency under load, a critical bufferbloat parameter. The CPU details can also be used to verify idle conditions or examine CPU frequency against ping variations and jitter (e.g. tracing high jitter to occasional low CPU frequency operation).

After running `speedtest-netperf.sh`, if latency is seen to increase much during the data transfers, then other network activity, such as voice or video chat, gaming, and general interactive usage will likely suffer. Gamers will see this as frustrating lag when someone else uses the network, Skype and FaceTime users will see dropouts or freezes, and VOIP service may be unusable.

## Installation

This package and its dependencies should be installed from the official OpenWrt software repository with the command:
`opkg install speedtest-netperf`

If unavailable, search for and try to directly download the same package for a newer OpenWrt release, since it is architecture-independent and very portable.

As a last resort, you may download and install the latest version directly from the author's personal repo: e.g.
```
cd /tmp
uclient-fetch https://github.com/guidosarducci/papal-repo/raw/master/speedtest-netperf_1.0.0-1_all.ipk
opkg install speedtest-netperf_1.0.0-1_all.ipk
```

## Usage

The speedtest-netperf.sh script measures throughput, latency and CPU usage during file transfers and when idle. To invoke it:

    speedtest-netperf.sh [-4 | -6] [-H netperf-server] [-t duration] [-p host-to-ping] [-n simultaneous-streams ] [-s | -c [duration] ] [ -i [duration] ]

Options, if present, are:

    -4 | -6:           Enable ipv4 or ipv6 testing (default - ipv4)
    -H | --host:       DNS or Address of a netperf server (default - netperf.bufferbloat.net)
                       Alternate servers are netperf-east (US, east coast),
                       netperf-west (US, California), and netperf-eu (Denmark).
    -t | --time:       Duration for how long each direction's test should run - (default - 30 seconds)
    -p | --ping:       Host to ping to measure latency (default - one.one.one.one)
    -n | --number:     Number of simultaneous sessions (default - 5 sessions)
    -s | --sequential: Sequential download/upload (default - disabled)
    -c | --concurrent: Concurrent download/upload (default - disabled)
    -i | --idle:       Measure idle latency before speed test (default - disabled)

The primary script output shows download and upload speeds, together with the percent packet loss, and a summary of latencies, including min, max, average, median, and 10th and 90th percentiles so you can get a sense of the distribution.

The tool also summarizes CPU usage statistics during the test, to highlight whether speeds may be CPU-bound during testing, and to provide a better sense of how much CPU "headroom" would be available during normal operation. The data includes per-CPU load and frequency (if supported), and CPU usage of the `netperf` test programs.

### Examples
Below is a comparison of sequential speed testing runs showing the benefits of SQM. On the left is a test without SQM. Note that the latency gets large (greater than half a second), meaning that network performance would be poor for anyone else using the network. On the right is a test using SQM: the latency goes up a little (less than 21 msec under load), and network performance remains good.

Notice also that the activation of SQM requires greater CPU, but that in both cases the router is not CPU-bound and likely capable of supporting higher throughputs.

```
/* Detailed source-code truncated for AI context efficiency. */
```

Below is another comparison of SQM, but now using a concurrent speedtest. Notice that without SQM, the total throughput drops nearly 11 Mbps compared to the above sequential test without SQM. This is due to both poorer latencies and the consumption of bandwidth by ACK messages. As before, the use of SQM on the right not only yields a marked improvement in latencies, but also recovers almost 6 Mbps in throughput (with SQM using CAKE's ACK filtering).
```
/* Detailed source-code truncated for AI context efficiency. */
```

## Provenance

The `speedtest-netperf.sh` utility leverages earlier scripts from the CeroWrt project used to measure network throughput and latency: [betterspeedtest.sh](https://github.com/richb-hanover/OpenWrtScripts#betterspeedtestsh) and [netperfrunner.sh](https://github.com/richb-hanover/OpenWrtScripts#netperfrunnersh). Both scripts are gratefully used with the permission of their author, [Rich Brown](https://github.com/richb-hanover/OpenWrtScripts).

---

### Overview (`net/stubby/files/README.md`)

# Stubby for OpenWrt

## Stubby Description

[Stubby](https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Daemon+-+Stubby) is
an application that acts as a local DNS Privacy stub resolver (using
DNS-over-TLS). Stubby encrypts DNS queries sent from a client machine to a DNS
Privacy resolver increasing end user privacy.

Stubby is useful on an OpenWrt device, because it can sit between the usual DNS
resolver (dnsmasq by default) and the upstream DNS resolver and be used to
ensure that DNS traffic is encrypted between the OpenWrt device and the
resolver.

Stubby is developed by the [getdns](http://getdnsapi.net/) project.

For more background and FAQ see the [About
Stubby](https://dnsprivacy.org/wiki/display/DP/About+Stubby) page.


## Installation

Installation of this package can be achieved at the command line using `opkg
install stubby`, or via the LUCI Web Interface. Installing the stubby package
will also install the required dependency packages, including the
`ca-bundle` package.

## Configuration

The default configuration of the package has been chosen to ensure that stubby
should work after installation.

By default, configuration of stubby is integrated with the OpenWrt UCI system
using the file `/etc/config/stubby`. The configuration options available are
also documented in that file. If for some reason you wish to configure stubby
using the `/etc/stubby/stubby.yml` file, then you simply need to set `option
manual '1'` in `/etc/config/stubby` and all other settings in
`/etc/config/stubby` will be ignored.

### Stubby port and addresses

The default configuration ensures that stubby listens on port 5453 on the
loopback interfaces for IPv4 and IPv6. As such, by default, stubby will respond
only to lookups from the OpenWrt device itself.

By setting the listening ports to non-standard values, this allows users to keep
the main name server daemon in place (dnsmasq/unbound/etc.) and have that name
server forward to stubby.

### Upstream resolvers

The default package configuration uses the CloudFlare resolvers, configured for
both IPv4 and IPv6. 

CloudFlare have not published SPKI pinsets, and even though they are available,
they have made no commitment to maintaining them. Using the currently known SPKI
pinsets for CloudFlare brings the risk that in the future they may be changed by
CloudFlare, and DNS would stop working. The default configuration has those SPKI
entries commented out for this reason.

[CloudFlare's privacy
statement](https://developers.cloudflare.com/1.1.1.1/commitment-to-privacy/)
details how they treat data from DNS requests.

More resolvers are available in the [upstream stubby example
configuration](https://github.com/getdnsapi/stubby/blob/develop/stubby.yml.example)
and the [DNS Privacy
list](https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Test+Servers).

## Integration of stubby with dnsmasq

The recommended way to use stubby on an OpenWrt device is to integrate it with a
caching resolver. The default caching resolver in OpenWrt is dnsmasq.

### Set dnsmasq to send DNS requests to stubby

Since dnsmasq responds to LAN DNS requests on port 53 of the OpenWrt device by
default, all that is required is to have dnsmasq forward those requests to
stubby which is listening on port 5453 of the OpenWrt device. To achieve this,
we need to set the `server` option in the dnsmasq configuration in the
`/etc/config/dhcp` file to `'127.0.0.1#5453'`. We also need to tell dnsmasq not
to use resolvers found in `/etc/resolv.conf` by setting the dnsmasq option
`noresolv` to `1` in the same file. This can be achieved by editing the
`/etc/config/dhcp` file directly or executing the following commands at the
command line:

    uci add_list dhcp.@dnsmasq[-1].server='127.0.0.1#5453'
    uci set dhcp.@dnsmasq[-1].noresolv=1
    uci commit && reload_config

The same outcome can be achieved in the LUCI web interface as follows:

1. Select the Network->DHCP and DNS menu entry.
2. In the "General Settings" tab, enter the address `127.0.0.1#5453` as the only
   entry in the "DNS Forwardings" dialogue.
3. In the "Resolv and Host files" tab tick the "Ignore resolve file" checkbox.

### Disable sending DNS requests to ISP provided DNS servers

The configuration changes in the previous section ensure that DNS queries are
sent over TLS encrypted connections *once dnsmasq and stubby are started*. When
the OpenWrt device is first brought up, there is a possibility that DNS queries
can go to ISP provided DNS servers ahead of dnsmasq and stubby being active. In
order to mitigate this leakage, it's necessary to ensure that upstream resolvers
aren't available, and the only DNS resolver used by the system is
dnsmasq+stubby. 

This requires setting the option `peerdns` to `0` and the option `dns` to the
loopback address for both the `wan` and `wan6` interfaces in the
`/etc/config/network` file. This can be achieved by editing the
`/etc/config/network` file directly, or by executing the following commands:

    uci set network.wan.peerdns='0'
    uci set network.wan.dns='127.0.0.1'
    uci set network.wan6.peerdns='0'
    uci set network.wan6.dns='0::1'
    uci commit && reload_config

The same outcome can also be achieved using the LUCI web interface as follows:

1. Select the Network->Interfaces menu entry.
2. Click on Edit for the WAN interfaces.
3. Choose the Advanced Settings tab.
4. Unselect the "Use DNS servers advertised by peer" checkbox
5. Enter `127.0.0.1` in the "Use custom DNS servers" dialogue box.
6. Repeat the above steps for the WAN6 interface, but use the address `0::1`
   instead of `127.0.0.1`.
   
### Enabling DNSSEC

The configuration described above ensures that DNS queries are executed over TLS
encrypted links. However, the responses themselves are not validated; DNSSEC
provides the ability to validate returned DNS responses, and mitigate against
DNS poisoning risks.

With the combination of stubby+dnsmasq there are two possible ways to enable
DNSSEC:

1. Configure stubby to perform DNSSEC validation, and configure dnsmasq to proxy
   the DNSSEC data to clients.
2. Configure stubby not to perform DNSSEC validation and configure dnsmasq to
   require DNSSEC validation.

Either option achieves the same outcome, and there appears to be little reason
for choosing one over the other other than that the second option is easier to
configure in the LUCI web interface. Both options are detailed below, and both
require that the `dnsmasq` package on the OpenWrt device is replaced with the
`dnsmasq-full` package. That can be achieved by running the following command:

    opkg install dnsmasq-full --download-only && opkg remove dnsmasq && opkg install dnsmasq-full --cache . && rm *.ipk

#### DNSSEC by stubby

Configuring stubby to perform DNSSEC validation requires setting the stubby
configuration option `dnssec_return_status` to `'1'` in `/etc/config/stubby`,
which can be done by editing the file directly or by executing the commands:

    uci set stubby.global.dnssec_return_status=1
    uci commit && reload_config
    
With stubby performing DNSSEC validation, dnsmasq needs to be configured to
proxy the DNSSEC data to clients. This requires setting the option `proxydnssec`
to 1 in the dnsmasq configuration in `/etc/config/dhcp`. That can be achieved by
the following commands:

    uci set dhcp.@dnsmasq[-1].proxydnssec=1
    uci commit && reload_config

#### DNSSEC by dnsmasq

Configuring dnsmasq to perform DNSSEC validation requires setting the dnsmasq
option `dnssec` to `1` in the `/etc/config/dhcp` file. In addition, it is
advisable to also set the dnsmasq option `dnsseccheckunsigned` to `1`. this can
be achieved by editing the file `/etc/config/dhcp` or by executing the following
commands:

    uci set dhcp.@dnsmasq[-1].dnssec=1
    uci set dhcp.@dnsmasq[-1].dnsseccheckunsigned=1
    uci commit && reload_config

The same options can be set in the LUCI web interface as follows:

1. Select the "Network->DHCP and DNS" menu entry.
2. Select the "Advanced Settings" tab.
3. Ensure both the "DNSSEC" and "DNSSEC check unsigned" check boxes are ticked.

#### Validating DNSSEC operation

Having configured DNSSEC validation using one of the two approaches above, it's
important to check it's actually working. The following command can be used:

    dig dnssectest.sidn.nl +dnssec +multi @192.168.1.1
    
This command should return output like the following:

    ; <<>> DiG 9.11.4-P1-RedHat-9.11.4-5.P1.fc28 <<>> dnssectest.sidn.nl +dnssec +multi @192.168.1.1
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 26579
    ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

    ;; OPT PSEUDOSECTION:
    ; EDNS: version: 0, flags: do; udp: 512
    ;; QUESTION SECTION:
    ;dnssectest.sidn.nl.	IN A

    ;; ANSWER SECTION:
    dnssectest.sidn.nl.	14399 IN A 213.136.9.12
    dnssectest.sidn.nl.	14399 IN RRSIG A 8 3 14400 (
				20181104071058 20181005071058 42033 sidn.nl.
				YAQl3tef36M9EQUOmCneHKCCkxox3csLpfUOql5i/6ND
				zPrQFsNr3g32HPoxOsi+hD2BE5+bEsnARayDSVLyx0qU
				6Hpi2rzQ0zGNZZkCJhCsdp3wnM1BWlMgPrCD0iIsJDok
				+DH5zu+yYufVUdSLQrMqA3MZDFUIqDUqSZuYDF4= )

    ;; Query time: 77 msec
    ;; SERVER: 192.168.1.1#53(192.168.1.1)
    ;; WHEN: Sat Oct 06 20:36:25 BST 2018
    ;; MSG SIZE  rcvd: 230

The key thing to note is the `flags: qr rd ra ad` part - the `ad` flag signifies
that DNSSEC validation is working. If that flag is absent DNSSEC validation is
not working.

## Appendix: stubby configuration options

This section details the options available for use in the `/etc/config/stubby`
file. The `global` configuration section specifies the configuration parameters
for the stubby daemon. One or more `resolver` sections are used to configure
upstream resolvers for the stubby daemon to use.

### `global` section options

#### `option manual`

Specify whether to use this file to configure the stubby service. If this is set
to `'1'` stubby will be configured using the file `/etc/stubby/stubby.yml`. If this
is set to `'0'`, configuration options will be taken from this file, and the service
will be managed through UCI.

#### `option trigger`

This specifies an interface to trigger stubby start up on; stubby startup will
be triggered by a procd signal associated with this interface being ready. If
this interface is restarted, stubby will also be restarted. 

This option can also be set to `'timed'`, in which case a time, specified by the
option `triggerdelay`, will be waited before starting stubby.


#### `option triggerdelay`

If the `trigger` option specifies an interface, this option sets the time that
is waited after the procd signal is received before starting stubby. 

If `trigger` is set to `'timed'` then this is the delay before starting stubby.
This option is specified in seconds and defaults to the value `'2'`.

#### `list dns_transport`

The `dns_transport` list specifies the allowed transports. Allowed values are:
`GETDNS_TRANSPORT_UDP`, `GETDNS_TRANSPORT_TCP` and `GETDNS_TRANSPORT_TLS`. The
transports are tried in the order listed.

#### `option tls_authentication`

This option specifies whether TLS authentication is mandatory. A value of `'1'`
mandates TLS authentication, and is the default.

If this is set to `'0'`, and `GETDNS_TRANSPORT_TCP` or `GETDNS_TRANSPORT_UDP`
appears in the `dns_transport` list, stubby is allowed to fall back to non-TLS
authenticated lookups. You probably don't want this though.

#### `option tls_query_padding_blocksize`

This option specifies the block size to pad DNS queries to. You shouldn't need
to set this to anything but `'128'` (the default), as recommended by
https://tools.ietf.org/html/draft-ietf-dprive-padding-policy-03

#### `option tls_connection_retries`

This option specifies the number of connection failures stubby permits before
Stubby backs-off from using an individual upstream resolver. You shouldn't need
to change this from the default value of `'2'`.

#### `option tls_backoff_time`

This option specifies the maximum time in seconds Stubby will back-off from
using an individual upstream after failures. You shouldn't need to change this
from the default value of `'3600'`.

#### `option timeout`

This option specifies the timeout on getting a response to an individual
request. This is specified in milliseconds. You shouldn't need to change this
from the default value of ` '5000'`.

#### `option dnssec_return_status`

This option specifies whether stubby should require DNSSEC validation. Specify
to `'1'` to turn on validation, and `'0'` to turn it off. By default it is off.

#### `option appdata_dir`

This option specifies the location for storing stubby runtime data. In
particular, if DNSSEC is turned on, stubby will store its automatically
retrieved trust anchor data here. The default value is `'/var/lib/stubby'`.

#### `option trust_anchors_backoff_time`

When Zero configuration DNSSEC failed, because of network unavailability or
failure to write to the appdata directory, stubby will backoff trying to refetch
the DNSSEC trust-anchor for a specified amount of time expressed in milliseconds
(which defaults to two and a half seconds).

#### `option dnssec_trust_anchors`

This option sets the location of the file containing the trust anchor data used
for DNSSEC validation. If this is not specified, stubby will automatically
retrieve a trust anchor at startup. It's unlikely you'll want to manage the
trust anchor data manually, so in most cases this is not needed. By default,
this is unset.

#### `option edns_client_subnet_private`

This option specifies whether to enforce ECS client privacy. The default is
`'1'`. Set to `'0'` to disable client privacy.

For more details see Section 7.1.2 [here](https://tools.ietf.org/html/rfc7871).

#### `option idle_timeout`

This option specifies the time (in milliseconds) to hold TLS connections open to
avoid the overhead of opening a new connection for every query. You should not
normally need to change this from the default value (currently `'10000'`).

See [here](https://tools.ietf.org/html/rfc7828) for more details.

#### `option round_robin_upstreams`

This option specifies how stubby will use the upstream DNS resolvers. Set to
`'1'` (the default) to instruct stubby to distribute queries across all
available name servers - this will use multiple simultaneous connections which
can give better performance in most (but not all) cases. Set to `'0'` to treat
the upstream resolvers as an ordered list and use a single upstream resolver
until it becomes unavailable, then use the next one.

#### `list listen_address`

This list sets the addresses and ports for the stubby daemon to listen for
requests on. the default configuration configures stubby to listen on port 5453
on the loopback interface for both IPv4 and IPv6.

#### `option log_level`

If set, this option specifies the level of logging from the stubby
daemon. By default, this option is not set.

The possible levels are:

    '0': EMERG  - System is unusable
    '1': ALERT  - Action must be taken immediately
    '2': CRIT   - Critical conditions
    '3': ERROR  - Error conditions
    '4': WARN   - Warning conditions
    '5': NOTICE - Normal, but significant, condition
    '6': INFO   - Informational message
    '7': DEBUG  - Debug-level message

#### `option command_line_arguments`

This option specifies additional command line arguments for
stubby daemon. By default, this is an empty string.

#### `option tls_cipher_list`

If set, this specifies the acceptable ciphers for DNS over TLS. With OpenSSL
1.1.1 this list is for TLS1.2 and older only. Ciphers for TLS1.3 should be set
with the `tls_ciphersuites` option. This option can also be given per upstream
resolver. By default, this option is not set.

#### `option tls_ciphersuites`

If set, this specifies the acceptable cipher for DNS over TLS1.3. OpenSSL
version 1.1.1 or greater is required for this option. This option can also be
given per upstream resolver. By default, this option is not set.

#### `option tls_min_version`

If set, this specifies the minimum acceptable TLS version. Works with OpenSSL
1.1.1 or greater only. This option can also be given per upstream resolver. By
default, this option is not set.

#### `option tls_max_version`

If set, this specifies the maximum acceptable TLS version. Works with OpenSSL
1.1.1 or greater only. This option can also be given per upstream resolver. By
default, this option is not set.


### `resolver` section options

#### `option address`

This option specifies the resolver IP address, and can either be an IPv4 or an
IPv6 address.

#### `option tls_auth_name`

This option specifies the upstream domain name used for TLS authentication with
the supplied server certificate

#### `option tls_port`

This option specifies the TLS port for the upstream resolver. If not specified,
this defaults to 853.

#### `option tls_cipher_list`

If set, this specifies the acceptable ciphers for DNS over TLS. With OpenSSL
1.1.1 this list is for TLS1.2 and older only. Ciphers for TLS1.3 should be set
with the `tls_ciphersuites` option. By default, this option is not set. If set,
this overrides the global value.

#### `option tls_ciphersuites`

If set, this specifies the acceptable cipher for DNS over TLS1.3. OpenSSL
version 1.1.1 or greater is required for this option. By default, this option is
not set. If set, this overrides the global value.

#### `option tls_min_version`

If set, this specifies the minimum acceptable TLS version. Works with OpenSSL
1.1.1 or greater only. By default, this option is not set. If set, this
overrides the global value.

#### `option tls_max_version`

If set, this specifies the maximum acceptable TLS version. Works with OpenSSL
1.1.1 or greater only. By default, this options is not set. If set, this
overrides the global value.

#### `list spki`

This list specifies the SPKI pinset which is verified against the keys in the
server certificate. The value takes the form `'<digest type>/value>'`, where
the `digest type` is the hashing algorithm used, and the value is the Base64
encoded hash of the public key. At present, only `sha256` is
supported for the digest type.

This should ONLY be used if the upstream resolver has committed to maintaining
the pinset. CloudFlare have made no such commitment, and so we do not specify
the SPKI values in the default configuration, even though they are available.

---

### Overview (`net/unbound/files/README.md`)

# Unbound Recursive DNS Server with UCI


## Unbound Description
[Unbound](https://www.unbound.net/) is a validating, recursive, and caching DNS resolver. The C implementation of Unbound is developed and maintained by [NLnet Labs](https://www.nlnetlabs.nl/). It is based on ideas and algorithms taken from a java prototype developed by Verisign labs, Nominet, Kirei and ep.net. Unbound is designed as a set of modular components, so that also DNSSEC (secure DNS) validation and stub-resolvers (that do not run as a server, but are linked into an application) are easily possible.

## Package Overview
OpenWrt default build uses [dnsmasq](http://www.thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html) for DNS forwarding and DHCP. With a forward only resolver, dependence on the upstream recursors may be cause for concern. They are often provided by the ISP, and some users have switched to public DNS providers. Either way may result in problems due to performance, "snoop-vertising", hijacking (MiM), and other causes. Running a recursive resolver or resolver capable of TLS may be a solution.

Unbound may be useful on consumer grade embedded hardware. It is fully DNSSEC and TLS capable. It is _intended_ to be a recursive resolver only. NLnet Labs [NSD](https://www.nlnetlabs.nl/projects/nsd/) is _intended_ for the authoritative task. This is different than [ISC Bind](https://www.isc.org/downloads/bind/) and its inclusive functions. Unbound configuration effort and memory consumption may be easier to control. A consumer could have their own recursive resolver with 8/64 MB router, and remove potential issues from forwarding resolvers outside of their control.

This package builds on Unbounds capabilities with OpenWrt UCI. Not every Unbound option is in UCI, but rather, UCI simplifies the combination of related options. Unbounds native options are bundled and balanced within a smaller set of choices. Options include resources, DNSSEC, access control, and some TTL tweaking. The UCI also provides an escape option and works at the raw `unbound.conf` level.

## HOW TO: Ad Blocking
The UCI scripts will work with [net/adblock](https://github.com/openwrt/packages/blob/master/net/adblock/files/README.md) and/or [net/adblock-fast](https://github.com/openwrt/packages/blob/master/net/adblock-fast/files/README.md), if they are installed and enabled. It's all detected and integrated automatically. In brief, the [net/adblock](https://github.com/openwrt/packages/blob/master/net/adblock/files/README.md) or [net/adblock-fast](https://github.com/openwrt/packages/blob/master/net/adblock-fast/files/README.md) create distinct local-zone files that are simply included in the unbound conf file during UCI generation. If you don't want this, then disable or reconfigure respective package to not send these files to Unbound.

With [net/adblock](https://github.com/openwrt/packages/blob/master/net/adblock/files/README.md) package specifically, a few additional tweaks may be needed to enhance the realiability and effectiveness. Ad Block option for delay time may need to be set for upto one minute (adb_triggerdelay), because of boot up race conditions with interfaces calling Unbound restarts. Also many smart devices (TV, microwave, or refigerator) will also use public DNS servers either as a bypass or for certain connections in general. If you wish to force exclusive DNS to your router, then you will need a firewall rule for example:

**/etc/config/firewall**:
```
config rule
  option dest 'wan'
  option dest_port '53 853 5353'
  option enabled '1'
  option family 'any'
  option name 'Block-Public-DNS'
  option proto 'tcpudp'
  option src 'lan'
  option target 'REJECT'
```

## HOW TO: Integrate with DHCP
Some UCI options and scripts help Unbound to work with DHCP servers to load the local DNS. The examples provided here are serial dnsmasq with unbound, parallel dnsmasq with unbound, and unbound scripted with odhcpd.

### Serial dnsmasq
In this case, dnsmasq is not changed *much* with respect to the default [OpenWrt](https://openwrt.org/docs/guide-user/base-system/dns_configuration) configuration. Here dnsmasq is forced to use the local Unbound instance as the lone upstream DNS server, instead of your ISP. This may be the easiest implementation, but performance degradation can occur in high volume networks. Unbound and dnsmasq effectively have the same information in memory, and all transfers are double handled.

**/etc/config/unbound**:
```
config unbound
  option add_local_fqdn '0'
  option add_wan_fqdn '0'
  option dhcp_link 'none'
  # dnsmasq should not forward your domain to unbound, but if...
  option domain 'yourdomain'
  option domain_type 'refuse'
  option listen_port '1053'
  ...
```

**/etc/config/dhcp**:
```
config dnsmasq
  option domain 'yourdomain'
  option noresolv '1'
  option port '53'
  option resolvfile '/tmp/resolv.conf.auto'
  list server '127.0.0.1#1053'
  list server '::1#1053'
  ...
```

### Parallel dnsmasq
In this case, Unbound serves your local network directly for all purposes. It will look over to dnsmasq for DHCP-DNS resolution. Unbound is generally accessible on port 53, and dnsmasq is only accessed at `127.0.0.1#1053` by Unbound. Although you can dig/drill/nslookup remotely with the proper directives.

**/etc/config/unbound**:
```
config unbound
  # likely you want to match domain option between Unbound and dnsmasq
  option dhcp_link 'dnsmasq'
  option domain 'yourdomain'
  option listen_port '53'
  ...
```

**/etc/config/dhcp**:
```
config dnsmasq
  option domain 'yourdomain'
  option noresolv '1'
  option port '1053'
  option resolvfile '/tmp/resolv.conf.auto'
  ...

config dhcp 'lan'
  # dnsmasq may not issue DNS option if not std. configuration
  list dhcp_option 'option:dns-server,0.0.0.0'
  ...
```

### Unbound and odhcpd
You may ask, "can Unbound replace dnsmasq?" You can have DHCP-DNS records with Unbound and [odhcpd](https://github.com/openwrt/odhcpd/blob/master/README) only. The UCI scripts will allow Unbound to act like dnsmasq. When odhcpd configures each DHCP lease, it will call a script. The script provided with Unbound will read the lease file for DHCP-DNS records. The unbound-control application is required, because simply rewriting conf-files and restarting unbound is too much overhead.
- Default OpenWrt has dnsmasq+odhcpd with `odhcpd-ipv6only` limited to DHCPv6.
- If you use dnsmasq+odhcpd together, then use dnsmasq serial or parallel methods above.
- You must install package `odhcpd` (full) to use odhcpd alone.
- You must install package `unbound-control` to load and unload leases.
- Remember to uninstall (or disable) dnsmasq when you won't use it.

**/etc/config/unbound**:
```
config unbound
  # name your router in DNS
  option add_local_fqdn '1'
  option add_wan_fqdn '1'
  option dhcp_link 'odhcpd'
  # add SLAAC inferred from DHCPv4
  option dhcp4_slaac6 '1'
  option domain 'lan'
  option domain_type 'static'
  option listen_port '53'
  option rebind_protection '1'
  # install unbound-control and set this
  option unbound_control '1'
  ...
```

**/etc/config/dhcp**:
```
config dhcp 'lan'
  option dhcpv4 'server'
  option dhcpv6 'server'
  option interface 'lan'
  option leasetime '12h'
  option ra 'server'
  option ra_management '1'
  ...

config odhcpd 'odhcpd'
  option leasefile '/var/lib/odhcpd/dhcp.leases'
  # this is where the magic happens
  option leasetrigger '/usr/lib/unbound/odhcpd.sh'
  option maindhcp '1'
```

## HOW TO: Manual Override
Yes, there is a UCI to disable the rest of Unbound UCI. However, OpenWrt or LEDE are targeted at embedded machines with flash ROM. The initialization scripts do a few things to protect flash ROM.

### Completely Manual (almost)
All of `/etc/unbound` (persistent, ROM) is copied to `/var/lib/unbound` (tmpfs, RAM). Edit your manual `/etc/unbound/unbound.conf` to reference this `/var/lib/unbound` location for included files. Note in preparation for a jail, `/var/lib/unbound` is `chown unbound`. Configure for security in`/etc/unbound/unbound.conf` with options `username:unbound` and `chroot:/var/lib/unbound`.

Keep the DNSKEY updated with your choice of flash activity. `root.key` maintenance for DNSKEY RFC5011 would be hard on flash. Unbound natively updates frequently. It also creates and destroys working files in the process. In `/var/lib/unbound` this is no problem, but it would be gone at the next reboot. If you have DNSSEC (validator) active, then you should consider the age UCI option. Choose how many days to copy from `/var/lib/unbound/root.key` (tmpfs) to `/etc/unbound/root.key` (flash).

**/etc/config/unbound**:
```
config unbound
  option manual_conf '1'
  option root_age '9'
  # end
```

### Hybrid Manual/UCI
You like the UCI. Yet, you need to add some difficult to standardize options, or just are not ready to make a UCI request yet. The files `/etc/unbound/unbound_srv.conf` and `/etc/unbound/unbound_ext.conf` will be copied to Unbounds chroot directory and included during auto generation.

The file `unbound_srv.conf` will be added into the `server:` clause. The file `unbound_ext.conf` will be added to the end of all configuration. It is for extended `forward-zone:`, `stub-zone:`, `auth-zone:`, and `view:` clauses. You can also disable unbound-control in the UCI which only allows "localhost" connections unencrypted, and then add an encrypted remote `control:` clause.

## HOW TO: Cache Zone Files
Unbound has the ability to AXFR a whole zone from an authoritative server to prefetch the zone. This can speed up access to common zones. Some may have special bandwidth concerns for DNSSEC overhead. The following is a generic example. UCI defaults include the [root](https://www.internic.net/domain/) zone, but it is disabled as a ready to go example.

**/etc/config/unbound**:
```
config zone
  option enabled '1'
  option fallback '1'
  list server 'ns1.it.example.com'
  list server 'ns2.it.example.com'
  option url_dir 'https://asset-management.it.example.com/zones/'
  option zone_type 'auth_zone'
  list zone_name 'example.com'
```

## HOW TO: TLS Over DNS
Unbound can use TLS as a client or server. UCI supports Unbound as a forwarding client with TLS. Servers are more complex and need manual configuration. This may be desired for privacy against stealth tracking. Some public DNS servers seem to advertise help in this quest. If your looking for a better understanding, then some information can be found at [Cloudflare](https://www.cloudflare.com/) DNS [1.1.1.1](https://1.1.1.1/). The following is a generic example. You can mix providers by using complete server specificaiton to override the zones common port and certificate domain index.

Update as of Unbound 1.9.1, all TLS functions work correctly with either OpenSSL 1.0.2 or 1.1.0. Please be sure to install `ca-bundle` package and use `opkg` to get updates regularly.

**/etc/config/unbound**:
```
config zone
  option enabled '1'
  # question: do you want to recurse when TLS fails or not?
  option fallback '0'
  option tls_index 'dns.example.net'
  option tls_port '853'
  option tls_upstream '1'
  # these servers assume a common TLS port/index
  list server '192.0.2.53'
  list server '2001:db8::53'
  # this alternate server is fully specified inline
  list server '192.0.2.153@443#dns.alternate.example.org'
  list zone_name '.'
  option zone_type 'forward_zone'
```

## Optional Compile Switches
Unbound can be changed by toggling switches within `make menuconfig` Libraries/Network/libunbound. Disable libevent, libpthread, and ipset to attempt to gain performance and size on small single core targets. These downgrade options are well tested, but they are not needed unless Unbound will not fit. Take care before enabling subnetcache, dnscrypt, and python options. These enhancements are not fully tested within OpenWrt and python is a large dependency. These enhancements are default off and they do not have UCI. You will need to use the files `/etc/unbound/unbound_srv.conf` and `/etc/unbound/unbound_ext.conf` to configure these modules. The `server:` clause line `module: subnetcache validator python iterator` will be filled out if the modules are compiled in.

Note: if you use python, then you will need to manual configure and you cannot use chroot. The scripts are not yet enhanced enough to set up the directory binding.

## Complete UCI
### config unbound
One instance is supported currently.
| UCI | Default | Units | Description | Unbound |
| --- | ------- | ----- | ----------- | ------- |
| add_extra_dns | 0 | level | Read OpenWrt traditional options for `dnsmasq`.<br>`0`: Disabled<br>`1`: Use only domain<br>`2`: Use domain, mxhost, and srvhost<br>`3`: Use all cname, domain, mxhost, and srvhost | local-data: |
| add_local_fqdn | 0 | level | Each level puts a  more detailed router entry within the LAN DNS (except link).<br>`0`: Disabled<br>`1`: Host name on the primary address<br>`2`: Host name on all addresses<br>`3`: FQDN and host name on all addresses<br>`4`: FQDN defined by "iface.hostname.domain" |  local-zone: local-data: |
| add_wan_fqdn | 0 | level | Same as `add_local_fqdn` but on WAN as listed in `iface_wan` | local-zone: local-data: |
| dns64 | 0 | boolean | Enable DNS64 RFC6052 to bridge IPv4 and IPv6 networks. | module: dns64 |
| dns64_prefix | 64:ff9b::/96 | subnet | DNS64 RFC6052 IPv4 in IPv6 well known prefix. | dns64-prefix: |
| dhcp_link | none | program | Link to a DHCP server with supported scripts. See HOW TO above. | local-zone: local-data: forward-zone: |
| dhcp4_slaac6 | 0 | boolean | Infer SLAAC IE64 IPv6 addresses from DHCPv4 MAC in DHCP link scripts. | - |
| exclude_ipv6_ga | 0 | boolean | If exclude IPv6 global addresses from local data. | local-data: |
| domain | lan | domain | This will suffix DHCP host records and be the default search domain. | local-zone: |
| domain_insecure | (empty) | domain | **List** domains that you wish to skip DNSSEC. It is one way around NTP chicken and egg. Your DHCP domains are automatically included. | domain-insecure: |
| domain_type | static | state | This allows you to lock down or allow forwarding of the local zone.<br>`static`: no forwarding like dnsmasq default<br>`refuse`: answer overtly with REFUSED<br>`deny`: covertly drop all queries<br>`transparent`: may continue forwarding or recusion | local-zone: |
| edns_size | 1232 | bytes | Extended DNS is necessary for DNSSEC. Use this to manage MTU issues. | edns-size: |
| extended_stats | 0 | boolean | Extended statistics are stored in Unbound memory for report by `unbound-control`. | extended-statistics: |
| hide_binddata  | 1 | boolean | Refuse possible attack queries like version.server, version.bind, id.server, and hostname.bind. | hide-identity: hide-version: |
| iface_lan | lan | interface | **List** to add interafaces you wish to consider to be LAN beyond those served by DHCP | interface: access-control: |
| iface_trig | lan wan | interface | **List** interfaces to watch IFUP to restart Unbound. This works around `netifd` and `procd` hyper activity with WAN DHCPv6 (else restart each 2-3 minutes). | - |
| iface_wan | wan | interface | **List** interafaces you wish to consider to be WAN for masked local zone purposes | interface-outgoing: |
| interface_auto | 1 | boolean | RECOMMEND ENABLED otherwise Unbound answers to any attached address regardless of query in-address. This also binds Unboud to the wild card address. | interface-automatic: |
| listen_port | 53 | port | Inbound port where Unbound will listen for queries. | port: |
| localservice | 1 | boolean | Prevent DNS amplification attacks. Only answer to subnets this machine has interfaces on. | access-control: |
| manual_conf | 0 | boolean | Skip all this UCI nonsense. Manually edit the configuration in `/etc/unbound/unbound.conf`. | - |
| num_threads | 1 | threads | Enable multithreading. Two are supported with only one cache slab. More becomes an industrial setup, and UCI simplificaitons may not be appropriate. | num-threads: |
| protocol | mixed | state | Limit Unbound protocols for recursion or forwarding.<br>`ip4_only`: old fashioned IPv4 upstream and downstream<br>`ip6_only`: test environment only; could cauase problems<br>`ip6_local`: upstream IPv4 only and local network IPv4 and IPv6<br>`ip6_prefer`: both IPv4 and IPv6 but try IPv6 first<br>`mixed`: both IPv4 and IPv6<br>`default`: built-in defaults | do-ip4: do-ip6: prefer-ip6: interface: |
| query_minimize | 0 | boolean | Enable a minor privacy option. Query one domain piece at a time (dot separator). | qname-minimisation: |
| query_min_strict | 0 | boolean | Query minimize may fall back to normal. This prevents the fall back, but non-standard name servers will fail. | qname-minimisation-strict: |
| rate_limit | 0 | query/s | Limit the total number of queries and queries per client by half that. Disabled by setting 0. | ratelimit: ip-ratelimit: |
| rebind_localhost | 0 | boolean | Prevent loopback addresses `127.0.0.0/8` or `::1` in upstream responses. They could be a good black hole server or a spring board attack. | private-address: |
| rebind_protection | 1 | level | Prevent your local addresses from being allowed in upstream responses. They could be a spring board attack.<br>`0`: Disabled<br>`1`: Only RFC 1918 and 4193 responses blocked<br>`2`: Above plus GLA /64 on machine<br>`3`: Above plus DHCP-PD passed down (not implemented) | private-address: |
| recursion | passive | state | Unbound has many options for recrusion but UCI is bundled for simplicity.<br>`passive`: slower until cache fills but kind on CPU load<br>`default`: built-in defaults<br>`aggressive`: uses prefetching to handle more requests quickly | (many) |
| resource | small | state | Unbound has many options for memory resources but UCI is bundled for simplicity.<br>`tiny`: similar to published memory restricted configuration<br>`small`: about half of medium<br>`medium`: similar to default<br>`default`: built-in defaults<br>`large`: about double of medium | \*-cache-size: |
| root_age | 9 | day | >90 Disables. Age limit for root data like root DNSSEC key. Scripts will copy from `tmps` to flash ROM with this limit to save write life. | - |
| ttl_min | 120 | second | Minimum TTL in cache to avoid abused low TTL for snoop-vertising and non-standard load balancing. Typical to configure maybe 0~300 but 1800 is the maximum accepted. | cache-min-ttl: |
| unbound_control | 0 | level | Enables `unbound-control` application access ports.<br>`0`: None else add your own in unbound_ext.conf<br>`1`: Unencrypted Local Host Access<br>`2`: SSL Local Host Access w/ auto unbound-control-setup<br>`3`: SSL Network Access w/ auto unbound-control-setup<br>`4`: SSL Network Access; static key/pem files must already exist | unbound-control: ... (clause) |
| validator | 0 | boolean | Enable DNSSEC validator module. | module: validator |
| validator_ntp | 1 | boolean | Disable DNSSEC time checks at boot and restart when NTP locks in time. DNSSEC requires time validation and this breaks the chicken and egg. | - |
| verbosity | 1 |  level | Logging inensity. | verbosity: |

### config zone
Confingure any mix of Unbound `forward-zone:`, `stub-zone:`, or `auth-zone:` clauses. These sections are more compact than Unbound and will unroll into Unbound's configuration syntax.
| UCI | Default | Units | Description | Unbound |
| --- | ------- | ----- | ----------- | ------- |
| dns_assist | none | program | Check against local host forwarding by requiring a target program to exist and be enabled else do not permit forwarding `127.0.0.0/8` or `::1`. Includes bind, dnsmasq, http-proxy-dns, ipset-dns, and nsd. | forward-addr: |
| enabled | 0 | boolean | turn zone on or off without deleting it | - |
| fallback | 1 | boolean | Allow this zone to fall through to other zones or recursion. | forward-first: |
| port | 53 | port | Target server's target port for plain DNS operations. | (auto 192.0.2.53 \#53)
| resolv_conf | 0 | boolean | Use resolv.conf servers as it was filled by the DHCP client. For example your ISP services (mail.example.net) or co-located services (streamed-movies.example.com). | - |
| server | (empty) | address domain | Required **list** to target servers. Full form is accepted but not required `192.0.2.53#53@dns.exmple.net`. Stub zones must use an IP address. | master: forward-host: forward-addr: stub-addr: |
| tls_index | (empty) | domain | TLS certifiicated signature index. If ommitted, Unbound encrypts but does not validate. | (auto 192.0.2.53 \#853 \@dns.example.net) |
| tls_port | 853 | port | Target server's target port for TLS DNS operations. | (auto 192.0.2.53 \#853) |
| tls_upstream | 0 | boolean | Use TLS to contact the zone server. | forward-tls-upstream: |
| url_dir | (empty) | string | Directory only path by http or https to get authoritative zone files. | url: |
| zone_name | (empty) | domain | Required **list** to select domains subject to this zone section. You can forward the root `.` zone or just your organizaiton `example.com.` | name: |
| zone_type | (empty) | state | Required option to create an Unbound zone clause.<br>`auth_zone`: prefetch whole zones from authoritative server<br>`forward_zone`: forward queries by domain name (like dnsmasq)<br>`stub_zone`: direct authoritative lookup by domain name | auth-zone: forward-zone: stub-zone: ... (clauses) |

---

--- METRICS ---
- Files Extracted: 39
- Estimated Token Budget: ~94509 tokens
- Canonical Reference: https://codewiki.google/github.com/openwrt/packages
