Programmable eBPF firewall for Go backends.
The Tholian Firewall blocks network traffic at the earliest possible point. It uses eBPF and XDP kernel modules that block the traffic before it can reach your operating system's networking stack.
It is designed to be embedded directly into a Go service as a library, and also ships as a standalone CLI for scripting and operations.
Traditional firewalls are configured out-of-band, live in a different process, and force every connection through a userspace decision or a rule interpreter.
The Tholian Firewall takes a different stance:
- In-kernel filtering: The XDP program drops matching packets at the driver receive path, so they never reach the network stack.
- Embeddable: A Go backend can call
actions.Forbid/actions.Permit/actions.Checkdirectly and control the firewall from within its own process. - Programmable: Targets are ordinary values like IP addresses, subnets, ports, ASNs, and domains. No external rule syntax.
- Verifier-safe by design: The kernel program is kept small and flat. Complex protocol work (like DNS answer extraction) is done in Go where it is cheap and testable.
- Fallbacks included: On platforms without eBPF,
iptables/ip6tablesand/etc/hostsare used instead.
- Filter ingress traffic at kernel speed using XDP, with a stable, tiny verifier memory footprint.
- Be a first-class Go library without CGo dependencies. No daemon required in the embedding process, no external rule engine.
- Cover the common blocking primitives like IPv4/IPv6 addresses, CIDR subnets, TCP/UDP ports, ASN-derived networks, and domains.
- Stay honest about state and the API always reports whether a ban was actually applied, and degrades to fallbacks when eBPF is unavailable.
- Privileged behaviour is exercised by Go integration tests which are compiled
into a single binary and run under
sudo.
- Linux with XDP support (
CONFIG_X86_64, a NIC/driver with XDP, or SKB mode). - Root, or
CAP_BPF+CAP_NET_ADMIN(map creation and XDP attach need them). - To build:
go,clang,llc/LLVM, and libbpf headers. - The embedded
source/insights/Internet.json.gzdataset forguardbuilds. It is intentionally not tracked by Git; see Building.
The shared structs, matchers, and types live in the separate tholian-core
module. This repository expects it to be checked out next to the firewall
folder:
tholian-network/
├── core/ # tholian-core module
└── firewall/ # this repository
source/go.mod resolves the dependency with
replace tholian-core => ../../core/source, which is relative to the source/
directory. If your checkout uses a different directory name or location, adjust
that replace directive accordingly.
cd /path/to/tholian-firewall;
bash make.sh ebpf;This compiles the eBPF module with -DENABLE_DNSFILTER,
emits LLVM IR, and generates little- and big-endian bytecode (module.bpfel,
module.bpfeb) that the Go bridge embeds. The generated artifacts are committed;
rebuild them whenever the C code changes.
cd /path/to/tholian-firewall;
bash make.sh source;
ls ./build/linux/*;
# tholian-firewall-guard-amd64
# tholian-firewall-guard-arm64
# tholian-firewall-guard_openwrt-amd64
# tholian-firewall-guard_openwrt-arm64| Tag | Includes |
|---|---|
guard (and guard_<distro>) |
eBPF adapter + insights (ASN data) |
guard_openwrt |
eBPF adapter only |
guard_freebsd / guard_netbsd / guard_openbsd |
iptables/hosts fallback |
intel / oversight |
eBPF stubs |
| no tag | eBPF stubs + iptables/hosts fallback |
sudo ./build/linux/tholian-firewall-guard-amd64 <action> [target]Targets use RFC 2732 syntax for IPv6 ([v6], [v6]/prefix, [v6]:port):
# Block an address, a subnet, and an exact socket
sudo tholian-firewall forbid "1.3.3.7";
sudo tholian-firewall forbid "1.3.3.0/24";
sudo tholian-firewall forbid "1.3.3.7:1338";
# IP:Port bans only the exact socket, not the whole address
sudo tholian-firewall forbid "1.3.3.7:1338";
sudo tholian-firewall check "1.3.3.7"; # still allowed
sudo tholian-firewall check "1.3.3.7:1338"; # forbidden
# Block an IPv6 target
sudo tholian-firewall forbid "[fe80::1337]/64";
sudo tholian-firewall forbid "[fe80::1337]:1338";
# Block a domain: drops its DNS responses and bans the resolved IPs
sudo tholian-firewall forbid "evil.example";
# Check (exit 0 = allowed, 1 = forbidden, 3 = no backend)
sudo tholian-firewall check "1.3.3.7";
# Look up the network/ASN data for an address
sudo tholian-firewall search "1.1.1.1";
# Attach to all interfaces, inspect current bans
sudo tholian-firewall init;
sudo tholian-firewall status;You can also apply a rule file:
# rules.txt
forbid 1.3.3.7;
forbid 1.3.3.0/24;
forbid evil.example;
permit 8.8.8.8;
sudo tholian-firewall load rules.txt;Tholian is a library first. Create a structs.Console for output, call
ebpf.Init(console) / insights.Init(console) to load the backends, then call
actions.Init(console) once at startup to attach to the interfaces and drive it
from your own code. The console is passed to every call so its message log stays
complete and can be serialized later:
import "tholian-firewall/actions"
import "tholian-firewall/adapters/mitigations/ebpf"
import "tholian-firewall/insights"
import "tholian-core/structs"
import "os"
func main() {
console := structs.NewConsole(os.Stdout, os.Stderr, 0)
ebpf.Init(console)
insights.Init(console)
if actions.Init(console) == false {
// no eBPF backend available; fall back or exit
}
actions.Forbid(console, "1.3.3.7")
actions.Forbid(console, "evil.example")
if actions.Check(console, "1.3.3.7") {
// traffic to 1.3.3.7 is currently dropped
}
actions.Permit(console, "1.3.3.7")
}Because unpinned BPF maps and XDP links live in the process that created them, the embedding process must stay alive for filtering to continue. Pinned maps and a daemon/IPC mode are planned; see docs/TODO.md.
Unit tests run anywhere; integration tests need root, a BPF-capable kernel, and the embedded Internet dataset:
bash test.shThe full log is written to logs/test.log. The suite runs formatting and vet
checks, unit tests with the race detector, builds a single integration test
binary with go test -c and runs it under sudo, then does CLI smoke tests.
See docs/TESTING.md for details.
- ARCHITECTURE.md documents the architecture, decisions, and the eBPF pitfalls (read this before changing the kernel module).
- TESTING.md documents how to run and read the tests.
- TODO.md documents the short-term task list.
- XDP is ingress-only; outbound traffic is not directly filtered.
- Maps and XDP links are process-scoped and not pinned (no persistence yet).
- Encrypted DNS (DoT/DoH) cannot be inspected; DNS-over-TCP is passed through.
- The embedded internet map is large (~31 MB gzip / ~825 MB raw) and parsed at startup.
permitremoves a ban; it cannot carve a hole in a broader ban.forbid IP:portcreates an exact socket ban. Does not ban the whole address.forbid domain:portstill bans the whole domain, not a single socket.- eBPF
statusprints domain bans as their FNV-1a hash, because the map only stores the hash, not the original name.
Proprietary.