🌐 Languages: English · Русский
Self-hosted transparent proxy manager for Raspberry Pi 4/5 (and any other Linux box). Drops in next to your router, intercepts LAN traffic via nftables TPROXY, and routes it through xray-core based on your rules — domain, GeoIP, GeoSite, MAC, port, protocol — with a web UI.
📸 Screenshots: jump to gallery.
- What it is
- Screenshots
- Architecture
- Features
- Supported protocols
- Quick start
- Configuration
- Development
- Tech stack
- Acknowledgements
- Contributing
- License
PiTun turns a small Linux box into a transparent proxy gateway for your home network. Devices that use the box as their default gateway have their outbound traffic intercepted at the kernel level, routed through one of several supported VPN protocols, and either tunnelled, sent direct, or dropped — all according to rules you set up in the web UI.
It was built for and primarily tested on the Raspberry Pi 4 / 5
(64-bit Raspberry Pi OS), but the project ships linux/amd64 images
too, so any Intel/AMD mini-PC, NUC, old laptop or x86_64 server that
can run Docker works just as well. Multi-arch images for both
linux/arm64 and linux/amd64 are produced by the
release workflow.
It's designed for the case where you want a single shared exit policy for the whole house (TVs, phones, IoT) without installing a client app on every device, and without depending on cloud-managed routers.
Three proxy endpoints exposed simultaneously, all sharing the same routing rule set:
| Endpoint | Default port | Use case |
|---|---|---|
| TPROXY | 7893 |
Transparent gateway — devices set the box as gateway |
| SOCKS5 | 1080 |
Explicit proxy for browsers and apps |
| HTTP | 8080 |
For apps without SOCKS5 support |
┌──────────────────────────────────────────────┐
Devices ────► │ PiTun host (RPi / mini-PC) │
(LAN) │ │
│ nftables TPROXY :7893 │
│ │ │
│ ▼ │
│ xray-core ─┬─ rules (geoip / geosite / │
│ │ domain / IP / MAC / port) │
│ │ │
│ ├─► proxy (VPN node / chain) │
│ ├─► direct (home router) │
│ └─► block │
│ │
│ + balancer groups (leastPing / random) │
│ + node circles (auto-rotate active node) │
│ + per-domain DNS (plain / DoH / DoT) │
└──────────────────────────────────────────────┘
Web UI talks to a FastAPI backend that owns the xray-core process, the nftables ruleset, and a SQLite database with all configuration. Frontend is a single-page React app served by nginx.
Core
- Transparent proxy via TPROXY + nftables, no per-device client
- SOCKS5 / HTTP proxies on the LAN
- Optional TUN mode and combined TPROXY+TUN
- QUIC (UDP/443) blocking — forces TCP fallback for protocols TPROXY can intercept
- Tunnel chaining — VLESS-inside-WireGuard, etc.
- Kill switch — drop all forwarded traffic if xray crashes
Routing
- Rule types:
mac,src_ip,dst_ip,domain,port,protocol,geoip,geosite - Actions:
proxy,direct,block,node:<id>,balancer:<id> - Drag-and-drop priority, bulk import, V2RayN/Shadowrocket JSON round-trip
- Per-MAC overrides ("this device always direct, that one always through node #5")
Health & resilience
- Background liveness probe with two-tier auto-failover: if the failed node belongs to an enabled NodeCircle, the failover handler delegates recovery to the circle (which skips dead siblings via pre-ping + retry); otherwise it walks a configurable fallback list
- Speed test per node via short-lived isolated xray instance
- Naive sidecar supervisor — auto-restarts crashed Naive containers with a sliding-window rate limiter
- Recent Events feed on Dashboard surfaces failovers, sidecar restarts, geo updates, circle rotations
Balancing & rotation
- Balancer groups (xray's
leastPingorrandomstrategies) - Node Circles — automatically rotate the active node on a schedule, seamlessly via xray's gRPC API (no dropped connections); each candidate is TCP-pinged with a single retry before switching, so dead siblings are skipped without a connection blip
Subscriptions
- Periodic refresh from VLESS / VMess / Trojan / SS / Hysteria2 / Clash YAML / xray JSON subscription URLs
- Per-subscription User-Agent (v2ray, clash, sing-box, happ, …), optional regex filter, configurable interval
Devices & DNS
- LAN discovery via
arp-scan, OUI vendor lookup - Per-device routing policy (default / always-include / always-bypass)
- Per-domain DNS rules (plain, DoH, DoT)
- FakeDNS pool for sniffing-friendly geoip resolution
- DNS query log with stats
Servers & deployments
- Inventory of remote VPS hosts (host, SSH credentials, tags) separate from runtime nodes — async-SSH probe, deployment records track which protocol/port is set up on which box, optional manual provisioning scripts (Caddy + naive, xray, SSH hardening) over the same SSH link
Operations
- One-click GeoIP / GeoSite refresh — three switchable upstream profiles: Loyalsoldier (CN-focused community list), runetfreedom (Russian-internet curated list), v2fly (vanilla baseline)
- Full-fidelity JSON Export/Import for Nodes and Servers — versioned bundle envelope, append/replace modes, optional secret redaction (separate from URI/subscription import which is single-node only)
- Built-in diagnostics page (DNS reachability, gateway, xray status, resource usage)
- Streaming xray log viewer
- Multi-language UI (English / Russian)
| Protocol | Notes |
|---|---|
| VLESS | Plain, TLS, REALITY, XTLS Vision, with WebSocket / gRPC / xhttp / HTTP/2 / HTTPUpgrade / mKCP / QUIC transports |
| VMess | Same transport menu as VLESS |
| Trojan | TLS / WebSocket / gRPC / xhttp |
| Shadowsocks | All modern stream / AEAD ciphers |
| WireGuard | Native xray outbound; works inside chains |
| Hysteria2 | UDP, with optional obfuscation password |
| SOCKS5 | As outbound (e.g. for chaining) |
| NaiveProxy | Per-node sidecar container (Caddy + forwardproxy on the server side); xray connects via local SOCKS5 |
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 64-bit ARM (RPi 4) or x86_64, 4 cores | RPi 5 / any modern x86_64 mini-PC |
| RAM | 1 GB | 2 GB+ (helps with naive sidecars and large geo data refresh) |
| Disk | 4 GB free | 8 GB+ (docker images + DB growth + DNS query log) |
| Network | 1 LAN interface, static IP, wired preferred | 1× wired GbE for LAN |
| OS | Any modern 64-bit Linux with kernel ≥ 5.4 (TPROXY support) | Raspberry Pi OS 64-bit, Debian 12+, Ubuntu 22.04+ |
| Architectures | linux/arm64 (RPi 4/5) · linux/amd64 (Intel/AMD mini-PC, NUC, x86_64 server) |
— |
- One of the supported architectures above
- Docker + Docker Compose v2
- Root access on the host (nftables + raw socket binding)
- A static LAN IP for the host
The simplest install is a single command that downloads everything, prepares the host, and brings up the stack. It pulls pre-built images from the latest GitHub Release, so no Docker build runs locally — total time is ~5 minutes on a fresh RPi, and the install resumes cleanly if the connection drops mid-way (every download is retried and atomically renamed).
curl -fsSL https://raw.githubusercontent.com/DaveBugg/PiTun/master/install.sh | sudo bashHeads up — passing flags to a piped script. The
--flagarguments below need to reach our installer, not bash. There are three working forms; pick the one that's hardest to mistype:(A) Foolproof — download then run:
curl -fsSL https://raw.githubusercontent.com/DaveBugg/PiTun/master/install.sh \ -o /tmp/pitun-install.sh sudo bash /tmp/pitun-install.sh --version v1.2.7(B) Pipe with
bash -s --separator (the-s --is required):curl -fsSL https://raw.githubusercontent.com/DaveBugg/PiTun/master/install.sh \ | sudo bash -s -- --version v1.2.7(C) Environment variable (no
-s --voodoo needed):curl -fsSL https://raw.githubusercontent.com/DaveBugg/PiTun/master/install.sh \ | sudo PITUN_VERSION=v1.2.7 bash❌ Do NOT do this:
curl ... | sudo bash --version v1.2.7— bash swallows--versionas its own flag (prints bash's version + exits) before our installer ever runs. Common copy-paste trap.
Useful flags (work via any of the three forms above; examples use form B):
# Pin a specific version
... | sudo bash -s -- --version v1.2.7
# Force rebuilding from source (no published release available, or
# you're testing local changes). Slower, needs reliable internet
# during the docker build.
... | sudo bash -s -- --build
# Air-gapped install — point at a directory containing pre-downloaded
# artifacts (pitun-{backend,naive,frontend}-vX.Y.Z-<arch>.tar.gz +
# pitun-src.tar.gz + xray.zip + geoip.dat + geosite.dat).
... | sudo bash -s -- --offline /tmp/pitun-artifacts
# Custom install path (default: /opt/pitun)
... | sudo bash -s -- --dir /srv/pitun
# Just preview what it would do — no changes made
... | sudo bash -s -- --dry-runAfter the script finishes:
- Web UI is at
http://<this-host-ip>/, loginadmin/password(change it on first login via Settings → Account). /opt/pitun/.envwas generated with a randomSECRET_KEYand the network block autodetected from your default-route interface:INTERFACE,LAN_CIDR,GATEWAY_IP(the PiTun host's own LAN IP),VITE_API_BASE_URL,VITE_WS_BASE_URL,CORS_ORIGINS. Verify withhead -30 /opt/pitun/.envbefore going to production; if anything looks off, edit anddocker compose -f /opt/pitun/docker-compose.yml restart.
See
install.sh --helpfor the full option list.
If you want the source tree alongside the running stack (e.g. for development, or to apply patches before deploy), the classic path still works:
git clone https://github.com/DaveBugg/PiTun pitun
cd pitun
# Host bootstrap: installs Docker (if missing), xray-core, GeoIP/GeoSite,
# system packages, kernel modules, sysctl tweaks, log rotation, daily
# cleanup cron. Skip if you'd rather do it manually — see "Manual install"
# below.
sudo bash scripts/setup.sh
cp .env.example .env
# Edit .env — at minimum set SECRET_KEY, INTERFACE, LAN_CIDR,
# GATEWAY_IP (the PiTun host's own LAN IP — what devices will use as
# their default gateway). A random SECRET_KEY: openssl rand -hex 32
#
# Tip: instead of editing manually, run `sudo bash install.sh
# --skip-host-prep` from the same checkout — it autodetects all the
# network values from your default-route interface and writes them
# into .env (only on first generation).
docker compose up -d --buildThe web UI listens on the host's LAN IP, port 80. Default login is
admin / password — change it on first run via Settings → Account.
If you'd rather wire the host yourself, here's the equivalent checklist.
Everything below must be done before docker compose up:
# 1. System packages
sudo apt update
sudo apt install -y curl wget ca-certificates nftables iproute2 \
net-tools iptables arp-scan dnsutils unzip jq cron
# 2. Free UDP/5353 (PiTun's DNS port)
sudo systemctl stop avahi-daemon avahi-daemon.socket || true
sudo systemctl disable avahi-daemon avahi-daemon.socket || true
sudo systemctl mask avahi-daemon || true
# 3. Sysctl: IP forwarding + TPROXY loopback
sudo tee /etc/sysctl.d/99-pitun.conf <<'EOF'
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
net.ipv4.conf.all.route_localnet = 1
EOF
sudo sysctl --system
# 4. TPROXY kernel modules (load now + pin for next boot)
sudo modprobe nft_tproxy xt_TPROXY
echo -e "nft_tproxy\nxt_TPROXY" | sudo tee /etc/modules-load.d/pitun.conf
# 5. Docker + Compose v2 (skip if already installed)
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER" # then log out + back in
# 6. GeoIP/GeoSite databases (bind-mounted RW into the backend container
# so the user can refresh them from the UI without an image rebuild).
# The xray binary itself is bundled inside the backend image as of
# v1.2.0 — no separate host install needed.
sudo mkdir -p /usr/local/share/xray
sudo curl -fsSL -o /usr/local/share/xray/geoip.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
sudo curl -fsSL -o /usr/local/share/xray/geosite.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
# 7. Static IP on the LAN interface (use NetworkManager, dhcpcd, or netplan
# depending on your distro; not scripted because the right tool varies).
# 8. Now you can deploy
cp .env.example .env && $EDITOR .env
docker compose up -d --buildWhy the geo databases are on the host, not inside the image.
geoip.datandgeosite.datare refreshable from the UI (GeoData → Update). Keeping them as a host bind-mount means a singlecurlupdates them in place — no image rebuild required. The xray binary itself, by contrast, is baked into the backend image as of v1.2.0 (used to be a host install). One less host-side prerequisite, version pinned to the release tag.
The CI release workflow publishes loadable Docker tarballs (linux/amd64 and linux/arm64) as GitHub Release assets. Useful for air-gapped / factory-fresh RPi installs:
# On a machine with internet
curl -LO https://github.com/DaveBugg/PiTun/releases/download/vX.Y.Z/pitun-backend-vX.Y.Z-arm64.tar.gz
curl -LO https://github.com/DaveBugg/PiTun/releases/download/vX.Y.Z/pitun-frontend-vX.Y.Z.tar.gz
# Transfer to the host, then:
docker load < pitun-backend-vX.Y.Z-arm64.tar.gz
tar -xzf pitun-frontend-vX.Y.Z.tar.gz -C frontend/dist/
docker compose up -dFor RPi-specific bootstrap (first boot, OS-level dependencies, network
config) scripts/ ships with helpers — see scripts/README.md.
All runtime config goes through the web UI. The only settings that
must be set before first start, via .env:
| Variable | Default | What |
|---|---|---|
SECRET_KEY |
changeme-… |
JWT signing key — openssl rand -hex 32 |
INTERFACE |
eth0 |
LAN interface name on the host |
LAN_CIDR |
192.168.1.0/24 |
Your LAN subnet (autodetected by install.sh) |
GATEWAY_IP |
192.168.1.100 |
The PiTun host's own LAN IP — devices set this as their default gateway. (Misnomer kept for backward compat; not the router's IP.) Autodetected by install.sh. |
BACKEND_PORT |
8000 |
Backend listen port (behind nginx) |
TPROXY_PORT_TCP |
7893 |
TPROXY TCP listener |
DNS_PORT |
5353 |
Internal DNS forwarder port |
NAIVE_PORT_RANGE_START |
20800 |
Allocator range for naive sidecars |
NAIVE_IMAGE |
pitun-naive:latest |
Image tag built locally or loaded from release |
Full annotated example: .env.example.
About
GATEWAY_IP: the variable name predates the LAN-gateway feature and refers to the PiTun host itself, not your home router. If the .env value disagrees with the actual interface IP, the backend auto-syncs the live IP into the database on the firstGET /settings, so the UI always shows the truth.LAN_CIDRhas the same runtime fallback as of 1.2.3.
# Backend
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt -r requirements-dev.txt
python -m uvicorn app.main:app --reload --port 8000
python -m pytest tests/ -q
# Frontend
cd frontend
npm ci
npm run dev # http://localhost:5173
npm run build # tsc + vite (catches type errors)
npm run test:ci
npm run lintThe full Docker stack lives in docker-compose.yml. For local UI work
without RPi-specific bits (TPROXY, nftables) you can skip Docker — auth,
nodes, routing rules and most of the UI work fine on macOS/Windows
against a backend running on localhost:8000.
See CONTRIBUTING.md for PR conventions and code
style.
Backend — Python 3.11, FastAPI, SQLModel/SQLAlchemy, Alembic, Pydantic v2, Uvicorn, httpx, aiohttp, aiosqlite, bcrypt, python-jose, psutil, docker-py, PyYAML.
Frontend — React 19, TypeScript, Vite, Tailwind CSS 3, TanStack Query (React Query) v5, Zustand, React Router 6, Recharts, Lucide React, axios, clsx, tailwind-merge.
Infrastructure — Docker + Compose, nginx (frontend), Tecnativa's docker-socket-proxy (read-only Docker API access from the backend), nftables, systemd.
Testing — pytest, Vitest, Testing Library.
PiTun is glue code on top of mature, hard-to-replicate upstream projects. Without them, none of this would exist:
- XTLS/Xray-core — the actual proxy engine. PiTun manages an xray-core process, generates its config, and talks to its gRPC API.
- klzgrad/naiveproxy —
Chromium-based HTTPS-tunnelling proxy used as a per-node sidecar.
PiTun's
docker/naive/builds an image from upstream releases. - Caddy with caddyserver/forwardproxy
(klzgrad's fork) — recommended NaiveProxy server.
scripts/setup-naive-server.shbuilds it viaxcaddy. - Loyalsoldier/v2ray-rules-dat
— GeoIP / GeoSite rule databases used by xray's
geoip:/geosite:matchers. PiTun pulls the latestgeoip.datandgeosite.datfrom here. - MaxMind GeoLite2 — GeoIP-MMDB lookups (optional, opt-in).
- netfilter / nftables — kernel-side TPROXY interception.
- arp-scan — LAN device discovery.
- FastAPI — HTTP framework
- SQLModel + SQLAlchemy — ORM
- Pydantic — validation
- Alembic — migrations
- Uvicorn — ASGI server
- httpx + aiohttp — HTTP clients
- aiosqlite — async SQLite
- python-jose + bcrypt — auth
- psutil — host metrics
- docker-py — Docker API client (Naive sidecar lifecycle)
- PyYAML — Clash YAML import
- React, Vite, TypeScript
- Tailwind CSS — styling
- TanStack Query — server state
- Zustand — UI state
- React Router — routing
- Recharts — metrics charts
- Lucide — icons
- axios — HTTP client
- Vitest + Testing Library — tests
- Docker + Compose
- nginx — frontend serving + WebSocket proxy
- Tecnativa/docker-socket-proxy — locked-down Docker API access for the backend
PiTun's import format compatibility (V2RayN / Shadowrocket / Clash JSON) is inspired by the formats of those projects — no code is borrowed.
Bug reports and PRs welcome. See CONTRIBUTING.md
for code style, PR conventions, and what to keep out of the repo.
BSD 3-Clause © PiTun contributors
Disclaimer. PiTun is a network management tool. You are responsible for complying with the laws of your jurisdiction and the terms of service of any upstream provider you use it with. The maintainers provide no warranty and accept no liability for misuse.






