# honk Quick Start honk is an experimental eBPF transparent proxy engine for Linux. Follow this guide to install, configure, and start it. Links at the end cover configuration fields and implementation details. > **Warning** > > honk is still alpha. Real mode loads eBPF programs, attaches TC and cgroup hooks, creates `dae0`/`daens`, changes sysctls, and uses NFQUEUE 320 by default. Keep an out-of-band management path available during the first deployment. ## Linux kernel requirements ### Kernel version ```shell uname -r ``` - Only Linux is supported. The real transparent datapath must run as `root`. - Linux `6.12+` is required for compiled routing and its synchronous map-in-map publication guarantee. Older kernels are rejected before attachment. - `netkit` is optional. honk falls back to veth when the kernel does not support it. `lan_interface` intercepts traffic entering the gateway from the LAN. `wan_interface` intercepts traffic originated by the gateway itself: - LAN only: intercept forwarded traffic only. - WAN only: intercept locally originated traffic only. - Both: intercept forwarded and locally originated traffic. - `wan_interface: auto` follows the lowest-metric IPv4 default route. ### Kernel configuration Desktop and server distributions usually enable the required features. Check them explicitly on minimal systems such as OpenWrt, Armbian, and VyOS. Display the current kernel configuration with: ```shell zcat /proc/config.gz 2>/dev/null || cat /boot/config-$(uname -r) ``` The real datapath requires the capabilities represented by at least these kernel options: ```text CONFIG_BPF=y CONFIG_BPF_SYSCALL=y CONFIG_BPF_JIT=y CONFIG_CGROUP_BPF=y CONFIG_NET_SCHED=y CONFIG_NET_CLS_ACT=y CONFIG_NET_XGRESS=y CONFIG_NET_NS=y ``` The TCX backend needs `CONFIG_NET_XGRESS`, a hidden option selected by `CONFIG_NET_CLS_ACT`, not legacy `CONFIG_NET_CLS_BPF` or `CONFIG_NET_SCH_INGRESS`. See [hook and qdisc ownership](./design/datapath.md#interface-hook-sets). Held-first-packet UDP, which is enabled by default, additionally requires: ```text CONFIG_NF_TABLES=y|m CONFIG_NF_TABLES_INET=y|m CONFIG_NFT_CT=y|m CONFIG_NFT_QUEUE=y|m CONFIG_NETFILTER_NETLINK_QUEUE=y|m ``` cgroup v2 with `CONFIG_CGROUP_BPF` is required. The real datapath attaches socket-identity hooks and the TCP `sock_ops` program `honk_tcp_passive` to the root cgroup and refuses to start without them. A legacy (flags=0) program already holding the root cgroup's `sock_ops` slot, for example one attached with `bpftool cgroup attach`, also makes startup fail. `honk_tcp_passive` is entered for every TCP `sock_ops` callback on the host, a few per connection, and returns at once unless the socket belongs to honk's namespace. TCP ownership proof needs no in-kernel BTF, no ftrace and no built-in IPv6, so a kernel that only has an external raw BTF file can run it. Startup always loads raw BTF (the eBPF loader's target BTF, plus the process-name and receive-trace offsets), from the file named by `HONK_VMLINUX_BTF`, else `/sys/kernel/btf/vmlinux`, else `/usr/lib/debug/boot/vmlinux` (where OpenWrt's `vmlinux-btf` package installs it). Only the optional `native-api` UDP receive-trace fallback, used when UDP receive-priority ancillary metadata is unavailable, attaches fentry/fexit programs. It needs the in-kernel BTF (`HONK_VMLINUX_BTF` does not substitute) and ftrace, and degrades without them: ```text CONFIG_DEBUG_INFO_BTF=y CONFIG_FUNCTION_TRACER=y CONFIG_DYNAMIC_FTRACE=y ``` A missing or incompatible `udp_recvmsg` or `udpv6_recvmsg` hook disables only that socket family's fallback; the shared `__skb_recv_udp` hook is required by both. A partial or failed fallback attachment is not retried on listener reload: restart after changing kernel/BTF support. See [receive-trace ownership](./design/control-plane.md#startup-and-shutdown). ### bpffs eBPF maps must be pinned on bpffs, not an ordinary directory: ```shell sudo install -d -m 0755 /sys/fs/bpf mountpoint -q /sys/fs/bpf || sudo mount -t bpf bpf /sys/fs/bpf mountpoint /sys/fs/bpf ``` If the system does not mount bpffs automatically, add this entry to `/etc/fstab`: ```fstab bpf /sys/fs/bpf bpf defaults 0 0 ``` ## Installation ### Prebuilt binaries Download the package matching the device architecture from [GitHub Releases](https://github.com/daeuniverse/honk/releases). Every name ends in its variant (older releases name the full build without a suffix): | Variant | Package suffix | Release label | Contents | |---|---|---|---| | full | `-full` | `fullapi` | Clash API, native API and the embedded doona UI (`x86_64`/`aarch64`, gnu and musl) | | clash-api | `-clash-api` | `clashapi` | Clash API only; no native API and no embedded UI (musl only) | | native-api | `-native-api` | `nativeapi` | Native API only; no Clash API and no embedded UI (musl only) | The musl packages are static binaries intended for gateways. Packages without the `-stock` suffix use mimalloc for throughput (label `mimalloc`); `-stock` packages use the system allocator and suit small devices where the RSS high-water mark matters more (label `stockalloc`). The Downloads section of each release lists every archive with its `{allocator}-{API}-{arch}-{libc}` label. The lean variants fail closed: enabling `experimental.native_api` on `-clash-api`, or `ui: embedded` without `native-ui`, fails startup. `--store db` and native configuration administration require `native-api`. ```shell pkg=honk-core---full # or -stock-full, -clash-api, -stock-native-api, as downloaded sha256sum -c --ignore-missing SHA256SUMS # SHA256SUMS comes from the same release tar -xzf "$pkg.tar.gz" sudo install -m 0755 "$pkg/honk-core" /usr/local/bin/honk-core honk-core --version ``` With the GitHub CLI installed, `gh attestation verify "$pkg.tar.gz" -R daeuniverse/honk` also confirms that the package was built by this repository's release workflow. Release binaries embed the eBPF object, so `honk-ebpf` does not need to be installed separately. At runtime, the engine does not invoke `ip`, `nft`, `iptables`, or `nsenter` subprocesses. ### Build from source A source build requires: - the stable Rust version in the root `rust-toolchain.toml` (`rustfmt`, `clippy`); - the nightly in `crates/honk-ebpf/rust-toolchain.toml` (`rust-src`, `llvm-tools`); - the `bpf-linker` version in `.github/ci/pins.env`; - a C/C++ toolchain, CMake, Clang, LLVM, libclang, libbpf headers, binutils, pkg-config, and Git; - network access to crates.io and GitHub. Debian/Ubuntu: ```shell sudo apt-get update sudo apt-get install -y \ build-essential clang llvm libbpf-dev pkg-config \ cmake libclang-dev binutils git curl ca-certificates ``` Arch Linux/CachyOS: ```shell sudo pacman -S --needed \ base-devel clang llvm libbpf cmake pkgconf git curl ca-certificates ``` Clone the repository, then install its pinned Rust toolchains: ```shell git clone https://github.com/daeuniverse/honk.git cd honk rustup toolchain install $(grep -oP '^channel\s*=\s*"\K[^"]+' rust-toolchain.toml) \ --profile minimal --component rustfmt --component clippy rustup toolchain install $(grep -oP '^channel\s*=\s*"\K[^"]+' crates/honk-ebpf/rust-toolchain.toml) \ --profile minimal --component rust-src --component llvm-tools source .github/ci/pins.env cargo install bpf-linker --version "$BPF_LINKER_VERSION" ``` Build the current `main` branch: ```shell unset RUSTFLAGS CARGO_ENCODED_RUSTFLAGS ( cd crates/honk-ebpf cargo build --release \ -Zbuild-std=core --target bpfel-unknown-none ) readelf -S crates/honk-ebpf/target/bpfel-unknown-none/release/honk-ebpf \ | grep -q '\.BTF' cargo build --release -p honk-core --features ebpf sudo install -m 0755 target/release/honk-core /usr/local/bin/honk-core ``` `RUSTFLAGS` overrides the eBPF crate's BTF flags. Aya cannot load an object without `.BTF`, so do not omit the environment cleanup or the check. ## Minimal configuration Start with a direct-only configuration to verify the datapath, then add subscriptions or proxy nodes. Replace `br-lan` with the LAN interface receiving client traffic. Remove `lan_interface` to intercept only local traffic. ```dae global { wan_interface: auto lan_interface: br-lan data_dir: '/var/lib/honk' log_level: info dial_mode: domain auto_config_kernel_parameter: true } routing { fallback: direct } ``` Install the configuration: ```shell sudo install -d -m 0700 /etc/honk /var/lib/honk sudo install -m 0600 /path/to/config.dae /etc/honk/config.dae ``` The default runtime root is `/var/lib/honk`. Existing artifacts under the legacy `/var/share/honk` root remain usable through the documented per-path fallback. honk does not automatically relocate them; writable state remains active in place. Custom `data_dir` values use the same fallback order. Relative caches, subscription stores, ECH/UI and other read-only dependencies check the configured directory, then `/var/share/honk`, then their caller-specific legacy path. Relative logs are created only below the configured directory. Start honk in the foreground: ```shell sudo /usr/local/bin/honk-core --config /etc/honk/config.dae ``` After `honk-core is running` appears, verify connectivity from a real LAN client before adding nodes, subscriptions, groups, routing, and DNS. The following is the smallest proxy structure; replace the address with a real node: ```dae node { edge: 'socks5://user:password@proxy.example.com:1080' } group { proxy { filter: name('edge') policy: fixed(0) } } routing { fallback: proxy } ``` When a proxy server uses a hostname, add an independent bootstrap resolver under `global` to avoid recursive DNS interception: ```dae bootstrap_resolver: '1.1.1.1:53' ``` See the [configuration guide](configuration.md) for the complete configuration flow. The repository's `config.dae`, `config.min.dae`, and `example.dae` contain example interfaces or nodes and must be adapted before deployment. ### GeoIP and Geosite Download these files only when the configuration references `geoip:` or `geosite:`: ```shell sudo curl -fL --retry 3 -o /var/lib/honk/geosite.dat \ https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat sudo curl -fL --retry 3 -o /var/lib/honk/geoip.dat \ https://github.com/v2fly/geoip/releases/latest/download/geoip.dat ``` The engine selects the first existing regular file in this order: `$DAE_LOCATION_ASSET/`, `/`, `/var/share/honk/`, `./`, `/usr/local/share/honk/`, `/usr/share/honk/`, `/usr/local/share/dae/`, `/usr/share/dae/`, then `/etc/dae/`. ## Run with systemd Create `/etc/systemd/system/honk-core.service`: ```ini [Unit] Description=honk transparent proxy engine Wants=network-online.target After=network-online.target [Service] Type=notify User=root WorkingDirectory=/var/lib/honk ExecStart=/usr/local/bin/honk-core --config /etc/honk/config.dae --disable-timestamp ExecReload=/usr/local/bin/honk-core reload Restart=on-failure RestartSec=2s TimeoutStopSec=30s LimitNOFILE=1048576 LimitMEMLOCK=infinity UMask=0077 [Install] WantedBy=multi-user.target ``` Start the service and enable it at boot: ```shell sudo systemctl daemon-reload sudo systemctl enable --now honk-core sudo systemctl status honk-core sudo journalctl -xefu honk-core ``` Do not add strict capability bounding, a read-only `/proc/sys`, or `NoNewPrivileges=yes` before validating it. Startup needs BPF, network administration, network namespace, mount, sysctl, and transparent-socket privileges. ## Check runtime status A running process does not prove that the datapath is usable. The logs should show that the real eBPF backend, interface hooks, listeners, and control plane are all ready. Each configured interface logs `configured interface is bound` at info level once its hooks are in place. An interface that does not exist yet, an `auto` entry without a default route, or a failed attach logs one warning (`configured interface is not bound yet` or `configured interface attach failed; retrying`) and then only at debug level; an unsupported link type logs one error (`configured interface is not supported`). `GET /runtime` reports the same condition as the `interface_binding` degradation (see the [API reference](./reference/api.md#native-api)). ```shell ip link show dae0 sudo bpftool prog show sudo bpftool map show sudo journalctl -u honk-core --since '5 minutes ago' ``` If `honk-tool` was built from source, run its read-only diagnosis: ```shell cargo build --release -p honk-tool sudo ./target/release/honk-tool diagnose \ --api http://127.0.0.1:9090 --pin-root /sys/fs/bpf ``` Use a real client to exercise direct and proxied TCP, UDP, and DNS traffic, plus IPv4/IPv6 where required. The presence of `dae0` or a reachable API is not an end-to-end traffic check. ## Reload and stop ```shell sudo systemctl reload honk-core # or sudo /usr/local/bin/honk-core reload ``` Use the running process logs and their `applied` or `rejected` result to determine whether the reload succeeded. Changes to interfaces, TPROXY settings, `data_dir`, the NFQUEUE switch, DNS bind, the Clash API listener/secret, cache settings, and other process-scoped fields require a restart. An interface that appears or disappears at runtime needs no reload: the watcher binds and releases it. Stop normally with: ```shell sudo systemctl stop honk-core ``` Do not use `kill -9` for routine shutdown. honk first closes datapath admission, fences NFQUEUE, and then removes hooks and links. Only one real datapath instance may run on a host; `/run/honk-core.lock` prevents fixed-resource conflicts. ## Unprivileged development `--mock-ebpf` can exercise configuration, API, DNS, and userspace outbounds, but it never intercepts real traffic. Save this configuration as `/tmp/honk.dae`: ```dae global { data_dir: '/tmp/honk' nfqueue_enable: false preconnect_node_count: 0 udp_warm_node_count: 0 } routing { fallback: direct } ``` ```shell cargo run --release -p honk-core -- \ --config /tmp/honk.dae --mock-ebpf ``` ## Current capabilities and limitations - Outbounds: Direct, Block, SOCKS5, Shadowsocks/2022, Trojan, AnyTLS, Hysteria2, TUIC, Juicity, VMess, and VLESS. - UDP: Direct, SOCKS5, Shadowsocks, Trojan, AnyTLS, Hysteria2, TUIC, Juicity, and VLESS when its independent packet permission is enabled. VLESS fallback encoding and multiplex carrier are separate choices; VMess remains TCP-only. See the [VLESS node reference](reference/nodes.md#vless-udp-and-multiplexing). - Groups: Selector, URLTest, LoadBalance, Fallback, and Score. - DNS upstreams: UDP, TCP, DoT, DoH, DoH3, and DoQ, optionally through a node or group. - `direct` and `block` are built-in nodes and must not be redeclared. - `nfqueue_enable` defaults to enabled, always uses queue 320, and exclusively owns `inet honk_nfqueue` / `udp_decision`. Firewall managers in the same network namespace must not modify those objects. - Mock builds and builds without the Cargo `ebpf` feature do not provide transparent proxying. Plain `cargo build --release` omits the real datapath; use `--features ebpf`. - VMess UDP, a rootless datapath, Dockerfile/docker-compose definitions, and non-Linux platforms are not currently supported. ## Troubleshooting | Symptom | Action | | --- | --- | | `bpf-linker` is missing | Install it as shown in the source-build section and add Cargo's bin directory to PATH. | | `no BTF parsed for object` | Clear `RUSTFLAGS` and `CARGO_ENCODED_RUSTFLAGS`, rebuild the eBPF object, and confirm `.BTF` with `readelf`. | | The kernel or verifier rejects compiled routing | Use Linux `6.12+` with BPF/BTF and freplace support. Keep the full verifier log and the generated rule attribution; do not bypass the startup failure. | | Pinning a map returns `Invalid argument` | `/sys/fs/bpf` is not bpffs; mount it as shown above. | | Mock mode starts but traffic bypasses honk | This is expected. Build with the `ebpf` feature and run as root. | | NFQUEUE is disabled or queue 320 is busy | Check nftables/NFQUEUE kernel support, the queue owner, and stale honk instances. Set `nfqueue_enable: false` only when staging is intentionally unnecessary. | | LAN traffic is not intercepted | `lan_interface` is empty or names the wrong interface. honk does not substitute `lo`. If the interface is named correctly, see the next row. | | `GET /runtime` shows an `interface_binding` degradation | A configured interface is not bound yet; the logs name it. `pending`: it does not exist or an `auto` entry has no IPv4 default route. `unsupported_type`: the link type is refused (for example `lo`, a VRF device or `dae0`); name a supported interface or a member. `attach_failed`: the TCX link could not be created, updated or verified; see the warning in the logs. See the [API reference](./reference/api.md#native-api). | | VLESS/VMess reports `No handler for protocol` | The build lacks `rprx`; use the default features or add `rprx` explicitly. | | A Geo rule fails to load | Put `geoip.dat`/`geosite.dat` in the configured `data_dir`, or verify the documented legacy/share search locations; remove unused Geo rules if not needed. | | A GNU binary does not execute on VyOS or a minimal system | Use the musl release for the same architecture. | ## Further reading - [Configuration guide](configuration.md) - [Global configuration](reference/global.md) - [Nodes and protocols](reference/nodes.md) - [Group policies](reference/groups.md) - [Routing rules](reference/routing.md) - [DNS configuration](reference/dns.md) - [CLI](reference/cli.md) - [Architecture overview](design/overview.md) ## Check parser changes From the repository root, run `just parser-ci` with the pinned stable Rust compiler, Go 1.26 or newer, Python 3.11 or newer, and network access. How to write a case is in `crates/honk-config/conformance/README.md`; the oracle and its licence are described in `tools/dae-parse/README.md`. It builds the pinned dae oracle, fetches upstream examples with manifest SHA256 checks, compares decoded structure and recorded dialect differences, then replays saved fuzz inputs. `just fuzz-replay` needs only stable Rust after Cargo dependencies are cached; it uses the same assertions as the fuzz targets. A replay failure prints the input path, and a hung input fails after five seconds. The `parser` CI lane runs for config code, the oracle, fuzz inputs, corpus source documents and their build inputs, or with `ci:full`. Ordinary workspace commands do not enable `conformance` or `fuzz-checks` and need neither Go nor nightly. The weekly job uses the nightly channel in `crates/honk-ebpf/rust-toolchain.toml` and cargo-fuzz pinned in `.github/ci/pins.env`. It runs `document`, `share_link`, and `lexer` for 480 seconds each with two workers and uploads findings and corpus files. Add a retained finding under `fuzz/artifacts//` to replay it on stable. Sanitizer-only findings still require the nightly fuzz target.