CachyDPI is a censorship-circumvention tool. It manipulates the structure of your own outbound packets (fragmentation, fake-packet injection) and drops forged packets injected by an ISP's DPI, so that DPI-based blocking of HTTP/TLS/QUIC fails. That is its entire purpose.
It is explicitly not:
- A privacy or anonymity tool. It does not encrypt your traffic. Your ISP still sees destination IPs and packet timing. For confidentiality or anonymity use a VPN or Tor.
- An exfiltration, scanning, or attack tool. It opens no inbound network service, sends no data anywhere on your behalf, and contains no remote-control channel. Its only IPC is a local, read-only metrics socket (below).
- A kernel or firewall replacement. It adds one isolated nftables table and removes it on exit; it does not alter your other firewall rules.
Bypassing ISP or state-mandated network filtering is regulated or prohibited in some jurisdictions. You are solely responsible for ensuring that running CachyDPI is lawful where you operate it. The software is provided under the GPL-3.0-or-later with no warranty (see LICENSE); the authors accept no liability for misuse or for legal consequences in your jurisdiction.
Please do not open a public issue for security problems. Use private disclosure:
- GitHub Security Advisory (preferred) — open a private advisory at
https://github.com/abel0x/cachydpi/security/advisories/new. - Maintainer contact — reach the maintainer via the GitHub profile @abel0x.
Include: affected component (e.g. nfqueue handler, TLS/HTTP parser, privilege management, config writer), reproduction steps, an impact assessment (privilege escalation, crash/DoS, information disclosure, config tampering), and a suggested fix if you have one.
| Action | Target (policy) |
|---|---|
| Acknowledge report | 48 hours |
| Initial assessment | 7 days |
| Patch release | 30 days (critical: 7 days) |
These targets are a best-effort policy for a single-maintainer project, not a contractual SLA.
| Version | Security updates |
|---|---|
| 1.0.x | ✅ |
| < 1.0 | ❌ |
CachyDPI requires exactly two Linux capabilities and nothing else (src/privileges.rs):
| Capability | Why |
|---|---|
CAP_NET_ADMIN |
NFQUEUE interception and nftables rule management |
CAP_NET_RAW |
Raw socket for injecting fragments / fake packets |
At startup the daemon verifies these are present (check_capabilities) and otherwise exits with guidance (setcap cap_net_admin,cap_net_raw=eip cachydpi). After it has installed the nftables rules and opened the queues, it reduces privilege:
Only CAP_NET_ADMIN is kept afterwards. CAP_NET_RAW is needed to open the raw sockets, and an open descriptor keeps working without it, so it is given up for the rest of the process lifetime; CAP_NET_ADMIN has to stay because teardown shells out to nft.
| Situation | Behaviour (drop_root) |
|---|---|
Already running as the cachydpi user (how the systemd unit runs) |
Drop every capability except CAP_NET_ADMIN |
Running as root under NoNewPrivileges |
setuid is blocked, so it drops capabilities only and logs a notice |
Running as root manually, cachydpi user exists |
PR_SET_KEEPCAPS → setgroups([]) → setgid → setuid(cachydpi) → restore CAP_NET_ADMIN to effective/permitted/inheritable → raise it into the ambient set → clear keepcaps → verify the result |
Running as root, cachydpi user missing |
Continue as root with CAP_NET_ADMIN only, log a warning |
The ambient raise is what makes the child nft process actually receive CAP_NET_ADMIN; the inheritable set alone grants a non-root child nothing, so without it teardown fails silently. verify_dropped_state then confirms the process is no longer root, holds CAP_NET_ADMIN in both the effective and ambient sets, and no longer holds CAP_NET_RAW — the daemon refuses to run on in an unclear privilege state rather than reporting a successful drop it cannot back up.
If dropping privileges fails, the process aborts — it will not continue with excess privilege (main.rs: the drop_root() error is fatal).
preflight_drop_root rehearses the whole transition in a forked child before any nftables rule is installed, so a uid that cannot be switched to is found while there is still nothing to clean up. Note that this only applies to the manual-root path: under the systemd unit the process starts as User=cachydpi already, so no setuid happens and the rehearsal does not run.
Under systemd this is reinforced from outside the process (systemd/cachydpi.service):
User=cachydpi
Group=cachydpi
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW
NoNewPrivileges=yesThe cachydpi user is a system user with no login shell (/usr/bin/nologin), created from systemd/cachydpi.sysusers with an auto-allocated UID (the sysusers line is u cachydpi - …, i.e. the UID is not hard-coded).
The unit applies the directives below. To see the resulting score on your system, run systemd-analyze security cachydpi.
| Area | Directives |
|---|---|
| Filesystem | ProtectSystem=strict, ProtectHome=yes, PrivateTmp=yes, PrivateDevices=yes, ReadOnlyPaths=/etc/cachydpi |
| Kernel | ProtectKernelTunables=yes, ProtectKernelModules=yes, ProtectKernelLogs=yes, ProtectControlGroups=yes, ProtectClock=yes, ProtectHostname=yes |
| Process | NoNewPrivileges=yes, RestrictSUIDSGID=yes, RestrictNamespaces=yes, LockPersonality=yes, RestrictRealtime=yes, MemoryDenyWriteExecute=yes, SystemCallArchitectures=native |
| Network | RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK AF_UNIX (AF_UNIX is needed for the metrics socket; AF_NETLINK for nftables/nfqueue) |
| Runtime dir | RuntimeDirectory=cachydpi, RuntimeDirectoryMode=0755 → /run/cachydpi/ |
Blacklist/exempt list file paths supplied via config or CLI are constrained before being read by the daemon:
- The literal path must start with
/etc/cachydpior/usr/share/cachydpi(rejects..in the path itself). - If the file exists, it is canonicalized and the resolved path must still start with an allowed prefix (defeats symlinks pointing outside the allowed directories).
- If it does not exist yet, the literal-prefix check stands.
A rejected path is logged and skipped, not loaded. This stops a malicious or mistaken config from making the root-capable daemon read arbitrary files (e.g. /etc/shadow) through the domain loader.
Scope note: this guard lives in the daemon's loader. The interactive
cachydpi settingscommand, which reads/counts the configured blacklist for its summary, does not currently re-apply this check — relevant only becausesettingsis run by an admin (typically viasudo) against a root-owned config. Hardeningsettingsto reuseis_safe_domain_pathis a tracked improvement.
queue_numis validated as0..=65534so the inbound queue (queue_num + 1) cannot overflow;modeis validated as1..=7(src/cli.rs).- Packet parsers (IP/TCP/TLS/HTTP) bound-check lengths and offsets before indexing; checksum helpers return early on undersized input. Rust's memory safety covers the rest.
The codebase is safe Rust except for narrowly scoped unsafe needed for C FFI and syscalls:
| Location | Purpose |
|---|---|
src/nfqueue/ |
FFI to libnetfilter_queue (nfq_*) — return values checked, null guards |
src/privileges.rs |
prctl (keepcaps, PR_SET_NAME, NoNewPrivileges probe), setuid/setgid |
src/rawsocket.rs / src/strategies/ |
raw packet send; socket created via the safe socket2 crate and marked SO_MARK |
The metrics server and the cachydpi-monitor TUI contain no unsafe and need no elevated privileges. The release profile compiles with panic = "abort" (Cargo.toml), removing unwinding machinery.
- Panic hook (installed in
main.rsbefore any network setup): on panic it prints the message and runsnft delete table ip cachydpi(andip6when IPv6 is enabled), then — because the build usespanic = "abort"— the process aborts. This keeps a crash from leaving stale queue rules behind. bypassflag on every nftables rule (queue num … bypass): if the daemon is not running (crash, stop, restart), packets bypass the queue instead of being dropped. A daemon failure does not black-hole traffic.- Graceful signals:
SIGTERM/SIGINT/SIGHUPare handled on a dedicated thread (not in an async-signal handler); rules are removed and queues closed on exit. - Restart limits (systemd):
Restart=on-failure,RestartSec=5,StartLimitBurst=5,StartLimitIntervalSec=60stop crash loops.
- Loop prevention: injected packets carry
SO_MARK = 0xCACD(src/rawsocket.rs), and the nftables rules begin withmeta mark 0xcacd accept, so CachyDPI never re-queues its own injected packets. - Table isolation: all rules live in the dedicated
ip cachydpi/ip6 cachydpitables. Cleanup deletes only those tables; existing firewall rules are untouched.
| Property | Value |
|---|---|
| Path | /run/cachydpi/metrics.sock (fallback /tmp/cachydpi-metrics.sock) |
| Protocol | JSON over a Unix stream socket |
| Direction | One-way, daemon → monitor (read-only) |
| Permissions | 0666 |
| Exposed | aggregate counters (packets processed, fragments sent, passive drops), timestamps, PID, process CPU/RAM, and the last 50 notable events |
| Not exposed | packet contents, config values, and — unless [logging] log_identifying_fields is on — IP addresses and domain names |
There is no control channel: the monitor can only read counters, never command the daemon. The directory is created by RuntimeDirectory=cachydpi and the socket is removed on shutdown / overwritten on restart.
The socket is 0666 so cachydpi monitor needs no privileges, which means everything in the snapshot is readable by any local process. The recent-events buffer used to embed the source address of each DPI-forged packet, making this a second route to the same data the journal was recording; those messages now go through the same redaction as the log. Anything added to the snapshot has to stay safe to hand to an unprivileged process.
CachyDPI sees exactly what a DPI box sees: the domain in every HTTP Host header and TLS SNI, and both endpoints of every connection. Writing that to a log turns a tool built to keep browsing off the wire into one that records it on disk — and under systemd the journal is usually persistent, so the record outlives the session. Anyone who attaches a journal excerpt to a bug report ships it elsewhere.
What is logged at each level:
| Level | Content |
|---|---|
info (default) |
Startup, configuration summary, nftables and privilege transitions, errors |
debug |
One line per intercepted packet: the strategy applied, fragment count and lengths, ports, payload sizes |
trace |
Per-fragment detail: split points, TTL calculations, packet header fields |
The level never decides whether identifying values are recorded. Raising it to diagnose a problem is routine and must not start capturing browsing history as a side effect, so two independent switches govern that:
| Switch | Default | Effect when on |
|---|---|---|
[logging] log_identifying_fields |
false |
Logs the domain/SNI of each processed connection and peer addresses. With it off these fields are still emitted, carrying <redacted>, so the log keeps a stable shape |
[logging] log_packet_bytes |
false |
Logs intercepted packet contents — including the TLS ClientHello — at trace |
Both announce themselves with a WARN at startup, because the realistic failure is turning one on to debug something and forgetting it.
Identifying values are never hashed. The SNI space is small and guessable, so a hash is reversible by dictionary in minutes: it would buy the appearance of protection and nothing else. A value is either logged or it is not.
The same redaction covers the metrics socket, since the recent-events buffer is served to any local process (see Metrics socket).
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
strip = true # no debug symbols in the release binary
panic = "abort" # no unwinding machineryDependencies are a small set of widely used crates (clap, serde/toml, serde_json, tracing + tracing-journald, nix, libc, caps, socket2, anyhow/thiserror, signal-hook, and optional ratatui/crossterm for the monitor). There is no async runtime and no network-client/TLS crate. CI runs cargo audit on every push (.github/workflows/ci.yml); run it yourself before building from source.
CachyDPI's git commits and release tags are signed with the maintainer's SSH signing key — the project does not use GPG. Every release tag is signed; only the first two commits, from before v1.0.0 was tagged, are not. Verify with git's SSH-signature support:
# Add the maintainer's published SSH signing key, mapped to their commit email:
echo '86494759+abel0x@users.noreply.github.com namespaces="git" ssh-ed25519 AAAA...PUBLICKEY...' \
>> ~/.config/git/allowed_signers
git config gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers
git verify-tag v1.9.0 # verify a signed release tag
git log --show-signature -1 # verify the tip commitFetch the exact public key from https://github.com/abel0x.keys and confirm it through an independent channel before trusting it. (Source-tarball distributions such as the AUR package do not currently ship a separate detached release signature; trust is anchored on the signed git tag.)
| Threat | Mitigation |
|---|---|
| Privilege escalation from the daemon | Non-root user, two-cap bounding set, NoNewPrivileges, fatal-on-failed-cap-drop |
| Malicious config → arbitrary file read | is_safe_domain_path restricts + canonicalizes domain-list paths |
| Malformed packet → crash / overflow | Bounds-checked parsers, Rust memory safety |
| Daemon crash → network black-hole | nftables bypass flag + panic-hook cleanup |
| Resource exhaustion | MemoryMax=64M, CPUQuota=25%, restart limits |
| Info disclosure via metrics socket | Read-only counters only; no packet/IP/domain data |
| Self-interception loop of injected packets | SO_MARK 0xCACD + meta mark accept rule |
| Code injection via W+X memory | MemoryDenyWriteExecute=yes |
| Privilege gain via SUID/exec/namespaces | NoNewPrivileges, RestrictSUIDSGID, RestrictNamespaces |
- Traffic confidentiality / anonymity — CachyDPI does not encrypt; use a VPN or Tor.
- Server-side and IP-level blocking, DNS poisoning — outside what client-side packet manipulation can address (see README → Limitations); configure DNS-over-TLS separately.
- Kernel compromise — no userspace hardening helps if the kernel is owned.
- Physical access to the host.
- Supply-chain attacks on dependencies — partially mitigated by
cargo audit; verify withcargo vetif needed.
- Install via the AUR package or
install.shso thecachydpisystem user and runtime dirs are created (systemd-sysusers/systemd-tmpfiles). - Keep config/blacklist root-owned and
0644; keep/etc/cachydpi0755 root. - Run as the systemd service (all hardening applies):
systemctl enable --now cachydpi. - If running the binary manually, prefer
setcap cap_net_admin,cap_net_raw=eipover running as root. - Configure DNS-over-TLS (README → DNS security).
-
cargo auditbefore building from source;systemd-analyze security cachydpiafter install.
Reporters of valid vulnerabilities are credited in release notes, with their permission.