honk Quick Start
Install, configure, and start honk, an experimental eBPF transparent proxy engine for Linux.
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
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. netkitis 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: autofollows 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:
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:
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=yThe 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.
Held-first-packet UDP, which is enabled by default, additionally requires:
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|mcgroup 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. Startup fails if either is unavailable. A legacy program with flags=0 already occupying the root cgroup's sock_ops slot also prevents startup, including one attached with bpftool cgroup attach.
honk_tcp_passive receives every host TCP sock_ops callback, a few per connection. It returns immediately for sockets outside honk's namespace. TCP ownership proof requires neither in-kernel BTF, ftrace nor built-in IPv6; an external raw BTF file is sufficient.
Startup always loads raw BTF for the eBPF loader's target BTF and the process-name and receive-trace offsets. It selects the file in this order:
- The file specified by
HONK_VMLINUX_BTF. /sys/kernel/btf/vmlinuxwhen no override is set./usr/lib/debug/boot/vmlinuxwhen the preceding default path is unreadable. OpenWrt'svmlinux-btfpackage installs this file.
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:
CONFIG_DEBUG_INFO_BTF=y
CONFIG_FUNCTION_TRACER=y
CONFIG_DYNAMIC_FTRACE=yA 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.
bpffs
eBPF maps must be pinned on bpffs, not an ordinary directory:
sudo install -d -m 0755 /sys/fs/bpf
mountpoint -q /sys/fs/bpf || sudo mount -t bpf bpf /sys/fs/bpf
mountpoint /sys/fs/bpfIf the system does not mount bpffs automatically, add this entry to /etc/fstab:
bpf /sys/fs/bpf bpf defaults 0 0Installation
Prebuilt binaries
Download the package matching the device architecture from GitHub 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.
| Allocator variant | Release label | Intended use |
|---|---|---|
No -stock suffix: mimalloc | mimalloc | Prioritize throughput. |
-stock: system allocator | stockalloc | Small devices where the RSS high-water mark matters more. |
Each release's Downloads section 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 or ui: embedded-lite without native-ui, fails startup. --store db and native configuration administration require native-api. embedded-lite selects the lighter doona lite edition (preview).
pkg=honk-core-<version>-<target>-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 --versionWith 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
The real-eBPF source build below uses:
- 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-linkerversion 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:
sudo apt-get update
sudo apt-get install -y \
build-essential clang llvm libbpf-dev pkg-config \
cmake libclang-dev binutils git curl ca-certificatesArch Linux/CachyOS:
sudo pacman -S --needed \
base-devel clang llvm libbpf cmake pkgconf git curl ca-certificatesClone the repository, then install its pinned Rust toolchains:
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:
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-coreRUSTFLAGS 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.
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:
sudo install -d -m 0700 /etc/honk /var/lib/honk
sudo install -m 0600 /path/to/config.dae /etc/honk/config.daeThe default runtime root is /var/lib/honk. Existing artifacts under the legacy /var/share/honk root remain usable through the documented per-path fallback. Legacy cache and subscription imports copy eligible state into state/honk.db. 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:
sudo /usr/local/bin/honk-core --config /etc/honk/config.daeAfter honk-core is running appears, verify connectivity from a real LAN client before adding nodes, subscriptions, groups, routing, and DNS. To apply the minimal proxy example:
- Keep the direct-only configuration's
globalsection. - Add the example's
nodeandgroupsections. - Replace the entire
routing { fallback: direct }section withrouting { fallback: proxy }. Do not append a secondroutingsection. - Replace the example address and credentials with a real node.
node {
edge: 'socks5://user:[email protected]: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:
bootstrap_resolver: '1.1.1.1:53'See the configuration guide 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.
Single-arm gateways
On a single-arm gateway, also called a bypass router, lan_interface and the WAN are the same interface or bridge: clients use the honk host as their gateway, and the host sends their traffic back out through that interface to the upstream router. Set the three kernel parameters below on such a host. honk does not set these three, and auto_config_kernel_parameter does not change that (see the global reference).
- With
send_redirectson, the kernel sends clients ICMP redirects that point them at the upstream router, and their traffic then bypasses honk. The kernel sends redirects when either theallor the interface value is1, so set both to0. - honk sets
rp_filterto0only on the first bound LAN interface, but the kernel applies the higher of thealland interface values, so a value of1or2underalloverrides it. Setallto0.
Replace br-lan with the interface named in lan_interface, then load the file:
sudo tee /etc/sysctl.d/60-honk-single-arm.conf <<'EOF'
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.br-lan.send_redirects = 0
net.ipv4.conf.all.rp_filter = 0
EOF
sudo sysctl -p /etc/sysctl.d/60-honk-single-arm.confThe file is also applied at every boot. Check the values; each should be 0:
sysctl net.ipv4.conf.all.send_redirects net.ipv4.conf.br-lan.send_redirects net.ipv4.conf.all.rp_filterA client that already received a redirect may keep the learned route for a while; reconnect it to the network before testing.
Web UI and APIs
Add one of the following blocks to the configuration, then restart honk. These examples listen only on the local machine. See the experimental reference for all other fields and reverse-proxy requirements.
doona (native API). The example below uses a full release, which embeds the UI. A build with native-api can instead serve a separately installed doona directory: set ui to that directory's path.
experimental {
native_api {
enabled: true
listen: '127.0.0.1:9527'
password_auth: true
config_write: true
ui: 'embedded'
}
}Open http://127.0.0.1:9527/ui/ after startup.
enabledstarts the native API listener;listensets its address and port.password_authenables password login. On the first visit, create the administrator account; honk stores it underdata_dirinstate/honk.db, not in the configuration file.config_writepermits authenticated configuration writes and reloads. Sources containing listener credentials remain read-only.ui: 'embedded'serves the standard doona bundled in the binary; it does not download a dashboard at startup.
doona lite. The embedded example uses the same build requirements, address and login procedure. This lighter edition is a preview. Choose this block instead of the standard doona block:
experimental {
native_api {
enabled: true
listen: '127.0.0.1:9527'
password_auth: true
config_write: true
ui: 'embedded-lite'
}
}Clash API for Clash dashboards. Use a build with clash-api enabled, such as the full or clash-api release. Replace the secret with a long random string:
experimental {
clash_api {
external_controller: '127.0.0.1:9090'
external_ui: 'zashboard'
secret: '<a long random string>'
}
}external_controller starts the Clash listener. external_ui selects the dashboard directory; honk downloads zashboard on first start when that directory is missing or empty. Open http://127.0.0.1:9090/ui/ and use the configured secret to connect the dashboard to the API. The API uses the secret for authentication; static UI files are public.
Both APIs can run together in a build that includes both features, such as full. Put native_api and clash_api beside each other inside one experimental block; keep their separate listen ports.
GeoIP and Geosite
Download these files only when the configuration references geoip: or geosite::
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.datThe engine selects the first existing regular file in this order:
$DAE_LOCATION_ASSET/<name><data_dir>/<name>/var/share/honk/<name>./<name>/usr/local/share/honk/<name>/usr/share/honk/<name>/usr/local/share/dae/<name>/usr/share/dae/<name>/etc/dae/<name>
Run with systemd
Create /etc/systemd/system/honk-core.service:
[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.targetStart the service and enable it at boot:
sudo systemctl daemon-reload
sudo systemctl enable --now honk-core
sudo systemctl status honk-core
sudo journalctl -xefu honk-coreDo 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.
Interface binding produces the following logs:
| Interface state | Log level and frequency | Message |
|---|---|---|
| Hooks are ready | info, once on binding | configured interface is bound |
Interface is absent, or auto has no default route | One warning, then debug | configured interface is not bound yet |
| Attachment fails | One warning, then debug | configured interface attach failed; retrying |
| Link type is unsupported | One error | configured interface is not supported |
GET /runtime reports unbound interfaces through the interface_binding degradation. See the API reference.
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:
cargo build --release -p honk-tool
sudo ./target/release/honk-tool diagnose \
--api http://127.0.0.1:9090 --pin-root /sys/fs/bpfUse 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
sudo systemctl reload honk-core
# or
sudo /usr/local/bin/honk-core reloadUse 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:
sudo systemctl stop honk-coreDo 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:
global {
data_dir: '/tmp/honk'
nfqueue_enable: false
preconnect_node_count: 0
udp_warm_node_count: 0
}
routing {
fallback: direct
}cargo run --release -p honk-core -- \
--config /tmp/honk.dae --mock-ebpfCurrent 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.
- Groups: Selector, URLTest, LoadBalance, Fallback, and Score.
- DNS upstreams: UDP, TCP, DoT, DoH, DoH3, and DoQ, optionally through a node or group.
directandblockare built-in nodes and must not be redeclared.nfqueue_enabledefaults to enabled, always uses queue 320, and exclusively ownsinet honk_nfqueue/udp_decision. Firewall managers in the same network namespace must not modify those objects.- Mock builds and builds without the Cargo
ebpffeature do not provide transparent proxying. Plaincargo build --releaseomits 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 native API reference for interface-state fields. |
| On a single-arm gateway, clients bypass honk or forwarded traffic is dropped | Set send_redirects to 0 for all and the LAN interface, and net.ipv4.conf.all.rp_filter to 0. See Single-arm gateways. |
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
- Global configuration
- Nodes and protocols
- Group policies
- Routing rules
- DNS configuration
- CLI
- Architecture overview
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. Case instructions are in crates/honk-config/conformance/README.md; oracle and licence details are in tools/dae-parse/README.md.
The command performs these checks in order:
- Build the pinned dae oracle.
- Fetch upstream examples and verify their manifest SHA256 values.
- Compare decoded structure and recorded dialect differences.
- Replay saved fuzz inputs.
With Cargo dependencies cached, just fuzz-replay needs only stable Rust. It uses the same assertions as the fuzz targets. A replay failure prints the input path; a hung input fails after five seconds.
The parser CI lane runs when configuration code, the oracle, fuzz inputs, corpus source documents or their build inputs change. It also runs with ci:full. Ordinary workspace commands enable neither conformance nor fuzz-checks, so they require neither Go nor nightly.
The weekly fuzz job uses:
| Setting | Value |
|---|---|
| Rust channel | Nightly pinned in crates/honk-ebpf/rust-toolchain.toml |
| cargo-fuzz version | Pinned in .github/ci/pins.env |
| Targets | document, share_link, lexer |
| Duration and concurrency | 480 seconds per target, two workers |
| Uploaded artifacts | Findings and corpus files |
Place a retained finding under fuzz/artifacts/<target>/ to replay it on stable. Sanitizer-only findings still require the nightly fuzz target.
Source: honk upstream, licensed under GPL-3.0-only.
