{"id":49670108,"url":"https://github.com/davebugg/pitun","last_synced_at":"2026-05-27T20:00:53.436Z","repository":{"id":356158758,"uuid":"1226510392","full_name":"DaveBugg/PiTun","owner":"DaveBugg","description":"Self-hosted gateway box that transparently routes your LAN through VPN/proxy nodes — by country, domain, or device. Built on xray-core + nftables, managed via web UI. Runs on Raspberry Pi or any Linux box.","archived":false,"fork":false,"pushed_at":"2026-05-20T14:50:06.000Z","size":3381,"stargazers_count":3,"open_issues_count":10,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-20T14:57:01.988Z","etag":null,"topics":["fastapi","home-network","naiveproxy","network-routing","nftables","privacy","router","self-hosted","tproxy","transparent-proxy","vless","vpn-gateway","wireguard","xray-core"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DaveBugg.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-01T13:48:21.000Z","updated_at":"2026-05-20T13:26:12.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/DaveBugg/PiTun","commit_stats":null,"previous_names":["davebugg/pitun"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/DaveBugg/PiTun","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DaveBugg%2FPiTun","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DaveBugg%2FPiTun/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DaveBugg%2FPiTun/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DaveBugg%2FPiTun/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DaveBugg","download_url":"https://codeload.github.com/DaveBugg/PiTun/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DaveBugg%2FPiTun/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33581559,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-27T02:00:06.184Z","response_time":53,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["fastapi","home-network","naiveproxy","network-routing","nftables","privacy","router","self-hosted","tproxy","transparent-proxy","vless","vpn-gateway","wireguard","xray-core"],"created_at":"2026-05-06T22:05:16.291Z","updated_at":"2026-05-27T20:00:53.428Z","avatar_url":"https://github.com/DaveBugg.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PiTun\n\n**🌐 Languages:** **English** · [Русский](README.ru.md)\n\n\u003e Self-hosted transparent proxy manager for Raspberry Pi 4/5 (and any\n\u003e other Linux box). Drops in next to your router, intercepts LAN\n\u003e traffic via nftables TPROXY, and routes it through xray-core based\n\u003e on your rules — domain, GeoIP, GeoSite, MAC, port, protocol — with a\n\u003e web UI.\n\n[![CI](https://img.shields.io/github/actions/workflow/status/DaveBugg/PiTun/ci.yml?branch=master\u0026label=CI)](#)\n[![License](https://img.shields.io/badge/license-BSD--3--Clause-blue)](LICENSE)\n[![Platform](https://img.shields.io/badge/platform-linux%2Famd64%20%7C%20linux%2Farm64-lightgrey)](#)\n\n📸 **Screenshots:** [jump to gallery](#screenshots).\n\n---\n\n## Table of contents\n\n- [What it is](#what-it-is)\n- [Screenshots](#screenshots)\n- [Architecture](#architecture)\n- [Features](#features)\n- [Supported protocols](#supported-protocols)\n- [Quick start](#quick-start)\n- [Server-side proxy install](#server-side-proxy-install)\n- [Configuration](#configuration)\n- [Troubleshooting](#troubleshooting)\n- [Uninstall](#uninstall)\n- [Development](#development)\n- [Tech stack](#tech-stack)\n- [Acknowledgements](#acknowledgements)\n- [Contributing](#contributing)\n- [License](#license)\n\n---\n\n## What it is\n\nPiTun turns a small Linux box into a **transparent proxy gateway** for\nyour home network. Devices that use the box as their default gateway\nhave their outbound traffic intercepted at the kernel level, routed\nthrough one of several supported VPN protocols, and either tunnelled,\nsent direct, or dropped — all according to rules you set up in the\nweb UI.\n\nIt was built for and primarily tested on the **Raspberry Pi 4 / 5**\n(64-bit Raspberry Pi OS), but the project ships **linux/amd64** images\ntoo, so any Intel/AMD mini-PC, NUC, old laptop or x86_64 server that\ncan run Docker works just as well. Multi-arch images for both\n`linux/arm64` and `linux/amd64` are produced by the\n[release workflow](.github/workflows/release.yml).\n\nIt's designed for the case where you want a single shared exit policy\nfor the whole house (TVs, phones, IoT) without installing a client app\non every device, and without depending on cloud-managed routers.\n\n**Three proxy endpoints exposed simultaneously, all sharing the same\nrouting rule set:**\n\n| Endpoint | Default port | Use case |\n|---|---|---|\n| TPROXY | `7893` | Transparent gateway — devices set the box as gateway |\n| SOCKS5 | `1080` | Explicit proxy for browsers and apps |\n| HTTP | `8080` | For apps without SOCKS5 support |\n\n## Screenshots\n\n\u003ca href=\"docs/screenshots/dashboard.jpg\"\u003e\n  \u003cimg src=\"docs/screenshots/dashboard.jpg\" alt=\"Dashboard\" width=\"800\"\u003e\n\u003c/a\u003e\n\n### VPS provisioning \u0026 x-ui orchestration (since v1.3.0)\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd width=\"50%\"\u003e\n      \u003ca href=\"docs/screenshots/servers.jpg\"\u003e\u003cimg src=\"docs/screenshots/servers.jpg\" alt=\"Servers\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eServers\u003c/b\u003e — VPS inventory, deployment badges (NaiveProxy / WireGuard / x-ui), one-click auto-install via SSH\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n    \u003ctd width=\"50%\"\u003e\n      \u003ca href=\"docs/screenshots/servers_tasks.jpg\"\u003e\u003cimg src=\"docs/screenshots/servers_tasks.jpg\" alt=\"Server tasks\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eServer tasks\u003c/b\u003e — live install logs streamed over WebSocket, status filters, captured tail for finalised jobs\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd width=\"50%\"\u003e\n      \u003ca href=\"docs/screenshots/xui.jpg\"\u003e\u003cimg src=\"docs/screenshots/xui.jpg\" alt=\"X-ui Panels\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eX-ui Panels\u003c/b\u003e — manage inbounds + clients on registered 3x-ui / x-ui-pro panels, healthcheck, sync, fakesite rotation\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n    \u003ctd width=\"50%\"\u003e\n      \u003ca href=\"docs/screenshots/chains.jpg\"\u003e\u003cimg src=\"docs/screenshots/chains.jpg\" alt=\"Proxy Chains\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eProxy Chains\u003c/b\u003e — two-hop VLESS+Reality across two x-ui panels, independent channels with their own SNI / Reality keys\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd width=\"50%\"\u003e\n      \u003ca href=\"docs/screenshots/deploy_modal.jpg\"\u003e\u003cimg src=\"docs/screenshots/deploy_modal.jpg\" alt=\"Deploy modal\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eDeploy modal\u003c/b\u003e — pick protocol (Naive / x-ui / WG), domain + LE email if needed, watch the install stream live\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n    \u003ctd width=\"50%\"\u003e\n      \u003ca href=\"docs/screenshots/chain_healthcheck.jpg\"\u003e\u003cimg src=\"docs/screenshots/chain_healthcheck.jpg\" alt=\"Chain healthcheck\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eChain healthcheck\u003c/b\u003e — panel API, xray state, inbound presence, relay routing, plus a live \u003ccode\u003etestOutbound\u003c/code\u003e probe of the relay→exit hop\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n### Routing engine \u0026 nodes\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd width=\"50%\"\u003e\n      \u003ca href=\"docs/screenshots/nodes.jpg\"\u003e\u003cimg src=\"docs/screenshots/nodes.jpg\" alt=\"Nodes\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eNodes\u003c/b\u003e — protocols, transports, latency, unified palette pills (protocol blue / transport green / reality purple / tls orange)\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n    \u003ctd width=\"50%\"\u003e\n      \u003ca href=\"docs/screenshots/routing.jpg\"\u003e\u003cimg src=\"docs/screenshots/routing.jpg\" alt=\"Routing\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eRouting\u003c/b\u003e — drag-priority rules, bulk import, V2RayN/Shadowrocket round-trip, multi-tag match-value editor\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd width=\"50%\"\u003e\n      \u003ca href=\"docs/screenshots/balancers.jpg\"\u003e\u003cimg src=\"docs/screenshots/balancers.jpg\" alt=\"Balancers\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eBalancers\u003c/b\u003e — group nodes with xray's \u003ccode\u003eleastPing\u003c/code\u003e / \u003ccode\u003erandom\u003c/code\u003e strategy\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n    \u003ctd width=\"50%\"\u003e\n      \u003ca href=\"docs/screenshots/circles.jpg\"\u003e\u003cimg src=\"docs/screenshots/circles.jpg\" alt=\"Node Circles\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eNode Circles\u003c/b\u003e — seamless rotation via xray gRPC API, TCP pre-ping with retry, two-tier auto-failover\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd width=\"50%\"\u003e\n      \u003ca href=\"docs/screenshots/subscription.jpg\"\u003e\u003cimg src=\"docs/screenshots/subscription.jpg\" alt=\"Subscriptions\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eSubscriptions\u003c/b\u003e — auto-update, per-OS Happ presets, custom UA override\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n    \u003ctd width=\"50%\"\u003e\n      \u003ca href=\"docs/screenshots/geodata.jpg\"\u003e\u003cimg src=\"docs/screenshots/geodata.jpg\" alt=\"Geo data profiles\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eGeo data\u003c/b\u003e — three switchable upstream profiles (Loyalsoldier / runetfreedom / v2fly) + scheduled refresh\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n### Devices, DNS \u0026 diagnostics\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd width=\"50%\"\u003e\n      \u003ca href=\"docs/screenshots/dns.jpg\"\u003e\u003cimg src=\"docs/screenshots/dns.jpg\" alt=\"DNS\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eDNS\u003c/b\u003e — per-domain rules, FakeDNS pool, query log + stats\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n    \u003ctd width=\"50%\"\u003e\n      \u003ca href=\"docs/screenshots/devices.jpg\"\u003e\u003cimg src=\"docs/screenshots/devices.jpg\" alt=\"Devices\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eDevices\u003c/b\u003e — LAN discovery, OUI vendors, per-device routing policy\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd colspan=\"2\" width=\"100%\"\u003e\n      \u003ca href=\"docs/screenshots/diagnostics.jpg\"\u003e\u003cimg src=\"docs/screenshots/diagnostics.jpg\" alt=\"Diagnostics\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eDiagnostics\u003c/b\u003e — DNS reachability, gateway state, xray health, resource snapshot, exportable bundle for bug reports\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd colspan=\"2\" width=\"100%\"\u003e\n      \u003ca href=\"docs/screenshots/settings.jpg\"\u003e\u003cimg src=\"docs/screenshots/settings.jpg\" alt=\"Settings\"\u003e\u003c/a\u003e\n      \u003cp align=\"center\"\u003e\u003csub\u003e\u003cb\u003eSettings\u003c/b\u003e — TPROXY / TUN / DNS / health check / GeoData scheduler / kill switch\u003c/sub\u003e\u003c/p\u003e\n    \u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n## Architecture\n\n```\n                 ┌──────────────────────────────────────────────┐\n  Devices  ────► │  PiTun host (RPi / mini-PC)                  │\n  (LAN)          │                                              │\n                 │  nftables TPROXY :7893                       │\n                 │       │                                      │\n                 │       ▼                                      │\n                 │  xray-core ─┬─ rules (geoip / geosite /      │\n                 │             │   domain / IP / MAC / port)    │\n                 │             │                                │\n                 │             ├─► proxy   (VPN node / chain)   │\n                 │             ├─► direct  (home router)        │\n                 │             └─► block                        │\n                 │                                              │\n                 │  + balancer groups (leastPing / random)      │\n                 │  + node circles (auto-rotate active node)    │\n                 │  + per-domain DNS (plain / DoH / DoT)        │\n                 └──────────────────────────────────────────────┘\n```\n\nWeb UI talks to a FastAPI backend that owns the xray-core process,\nthe nftables ruleset, and a SQLite database with all configuration.\nFrontend is a single-page React app served by nginx.\n\n## Features\n\n**Core**\n- Transparent proxy via TPROXY + nftables, no per-device client\n- SOCKS5 / HTTP proxies on the LAN\n- Optional TUN mode and combined TPROXY+TUN\n- QUIC (UDP/443) blocking — forces TCP fallback for protocols TPROXY\n  can intercept\n- Tunnel chaining — VLESS-inside-WireGuard, etc.\n- **Proxy Chains** (multi-panel, two-hop VLESS+Reality across two\n  x-ui panels with independent channels per chain; managed clients\n  + per-channel delete + live healthcheck)\n- Kill switch — drop all forwarded traffic if xray crashes\n\n**Routing**\n- Rule types: `mac`, `src_ip`, `dst_ip`, `domain`, `port`, `protocol`,\n  `geoip`, `geosite`\n- Actions: `proxy`, `direct`, `block`, `node:\u003cid\u003e`, `balancer:\u003cid\u003e`\n- Drag-and-drop priority, bulk import, V2RayN/Shadowrocket JSON\n  round-trip\n- Per-MAC overrides (\"this device always direct, that one always\n  through node #5\")\n\n**Health \u0026 resilience**\n- Background liveness probe with two-tier auto-failover: if the failed\n  node belongs to an enabled NodeCircle, the failover handler delegates\n  recovery to the circle (which skips dead siblings via pre-ping +\n  retry); otherwise it walks a configurable fallback list\n- Speed test per node via short-lived isolated xray instance\n- Naive sidecar supervisor — auto-restarts crashed Naive containers\n  with a sliding-window rate limiter\n- Recent Events feed on Dashboard surfaces failovers, sidecar\n  restarts, geo updates, circle rotations\n\n**Balancing \u0026 rotation**\n- Balancer groups (xray's `leastPing` or `random` strategies)\n- Node Circles — automatically rotate the active node on a schedule,\n  seamlessly via xray's gRPC API (no dropped connections); each\n  candidate is TCP-pinged with a single retry before switching, so\n  dead siblings are skipped without a connection blip\n\n**Subscriptions**\n- Periodic refresh from VLESS / VMess / Trojan / SS / Hysteria2 /\n  Clash YAML / xray JSON subscription URLs\n- Per-subscription User-Agent (v2ray, clash, sing-box, happ, …),\n  optional regex filter, configurable interval\n\n**Devices \u0026 DNS**\n- LAN discovery via `arp-scan`, OUI vendor lookup\n- Per-device routing policy (default / always-include / always-bypass)\n- Per-domain DNS rules (plain, DoH, DoT)\n- FakeDNS pool for sniffing-friendly geoip resolution\n- DNS query log with stats\n\n**Servers \u0026 deployments**\n- Inventory of remote VPS hosts (host, SSH credentials, tags) separate\n  from runtime nodes — async-SSH probe, deployment records track which\n  protocol/port is set up on which box, optional manual provisioning\n  scripts (Caddy + naive, xray, SSH hardening) over the same SSH link\n- One-click auto-deploy over SSH for **NaiveProxy**, **WireGuard**,\n  **x-ui** (3x-ui / x-ui-pro) — live log streaming, status badges,\n  cascade-clean on uninstall\n- Dedicated **X-ui Panels** page — full inbound + client management\n  (6 wired presets covering Reality / TLS / domain modes), live\n  healthcheck (panel API, xray, nginx, UFW, TLS cert, disk, mem),\n  cache↔panel sync for hand-added clients, random / custom\n  fakesite rotation\n\n**Operations**\n- One-click GeoIP / GeoSite refresh — three switchable upstream\n  profiles: Loyalsoldier (CN-focused community list), runetfreedom\n  (Russian-internet curated list), v2fly (vanilla baseline)\n- Full-fidelity JSON Export/Import for Nodes and Servers — versioned\n  bundle envelope, append/replace modes, optional secret redaction\n  (separate from URI/subscription import which is single-node only)\n- Plain-text URI export (`.txt`, one `vless://…` per line) — share\n  your node list with any v2rayN-compatible client; symmetric\n  `Import` button auto-detects URI list vs JSON bundle\n- Built-in diagnostics page (DNS reachability, gateway, xray status,\n  resource usage)\n- Streaming xray log viewer\n- Multi-language UI (English / Russian)\n\n## Supported protocols\n\n| Protocol | Notes |\n|---|---|\n| **VLESS** | Plain, TLS, REALITY, XTLS Vision, with WebSocket / gRPC / xhttp / HTTP/2 / HTTPUpgrade / mKCP / QUIC transports |\n| **VMess** | Same transport menu as VLESS |\n| **Trojan** | TLS / WebSocket / gRPC / xhttp |\n| **Shadowsocks** | All modern stream / AEAD ciphers |\n| **WireGuard** | Native xray outbound; works inside chains |\n| **Hysteria2** | UDP, with optional obfuscation password |\n| **SOCKS5** | As outbound (e.g. for chaining) |\n| **NaiveProxy** | Per-node sidecar container (Caddy + forwardproxy on the server side); xray connects via local SOCKS5 |\n\n## Quick start\n\n### System requirements\n\n| Resource | Minimum | Recommended |\n|---|---|---|\n| **CPU** | 64-bit ARM (RPi 4) or x86_64, 4 cores | RPi 5 / any modern x86_64 mini-PC |\n| **RAM** | 1 GB | 2 GB+ (helps with naive sidecars and large geo data refresh) |\n| **Disk** | 4 GB free | 8 GB+ (docker images + DB growth + DNS query log) |\n| **Network** | 1 LAN interface, static IP, wired preferred | 1× wired GbE for LAN |\n| **OS** | Any modern 64-bit Linux with kernel ≥ 5.4 (TPROXY support) | Raspberry Pi OS 64-bit, Debian 12+, Ubuntu 22.04+ |\n| **Architectures** | `linux/arm64` *(RPi 4/5)* · `linux/amd64` *(Intel/AMD mini-PC, NUC, x86_64 server)* | — |\n\n### Prerequisites\n\n- One of the supported architectures above\n- Docker + Docker Compose v2\n- Root access on the host (nftables + raw socket binding)\n- A static LAN IP for the host\n\n### Install — one-liner\n\nThe simplest install is a single command that downloads everything,\nprepares the host, and brings up the stack. It pulls pre-built images\nfrom the latest GitHub Release, so no Docker build runs locally —\ntotal time is ~5 minutes on a fresh RPi, and the install resumes\ncleanly if the connection drops mid-way (every download is retried\nand atomically renamed).\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/DaveBugg/PiTun/master/install.sh | sudo bash\n```\n\n\u003e **Heads up — passing flags to a piped script.** The `--flag` arguments\n\u003e below need to reach our installer, not bash. There are three working\n\u003e forms; pick the one that's hardest to mistype:\n\u003e\n\u003e **(A) Foolproof — download then run:**\n\u003e ```bash\n\u003e curl -fsSL https://raw.githubusercontent.com/DaveBugg/PiTun/master/install.sh \\\n\u003e      -o /tmp/pitun-install.sh\n\u003e sudo bash /tmp/pitun-install.sh --version v1.3.0-beta.8\n\u003e ```\n\u003e\n\u003e **(B) Pipe with `bash -s --` separator** (the `-s --` is **required**):\n\u003e ```bash\n\u003e curl -fsSL https://raw.githubusercontent.com/DaveBugg/PiTun/master/install.sh \\\n\u003e      | sudo bash -s -- --version v1.3.0-beta.8\n\u003e ```\n\u003e\n\u003e **(C) Environment variable** (no `-s --` voodoo needed):\n\u003e ```bash\n\u003e curl -fsSL https://raw.githubusercontent.com/DaveBugg/PiTun/master/install.sh \\\n\u003e      | sudo PITUN_VERSION=v1.3.0-beta.8 bash\n\u003e ```\n\u003e\n\u003e ❌ **Do NOT do this:** `curl ... | sudo bash --version v1.3.0-beta.8` — bash\n\u003e swallows `--version` as its own flag (prints bash's version + exits)\n\u003e before our installer ever runs. Common copy-paste trap.\n\nUseful flags (work via any of the three forms above; examples use form B):\n\n```bash\n# Pin a specific version (current: v1.3.0-beta.8)\n... | sudo bash -s -- --version v1.3.0-beta.8\n\n# Force rebuilding from source (no published release available, or\n# you're testing local changes). Slower, needs reliable internet\n# during the docker build.\n... | sudo bash -s -- --build\n\n# Air-gapped / hybrid offline install — point at a directory containing\n# pre-downloaded artifacts. ANY file present in the directory is used\n# as-is; missing ones fall back to a normal download (hybrid mode).\n# Also auto-detected when install.sh is launched from a directory that\n# already has any of the six expected filenames sitting next to it —\n# no --offline flag needed in that case. Full instructions and the\n# exact file list: docs/INSTALL_OFFLINE.md.\n... | sudo bash -s -- --offline /tmp/pitun-artifacts\n\n# Custom install path (default: /opt/pitun)\n... | sudo bash -s -- --dir /srv/pitun\n\n# Just preview what it would do — no changes made\n... | sudo bash -s -- --dry-run\n\n# Force IPv4 is the default. If you have a v6-only network, opt in:\n... | sudo bash -s -- --ipv6\n\n# Recover from a stale kill-switch lockup. ONLY when:\n#   - a previous PiTun run died with kill_switch=true active, AND\n#   - curl from this host now hangs on the very first download.\n# On a HEALTHY install with kill_switch + a running backend, omit\n# this flag — the install works over xray's normal bypass path.\n# The pre-flight detects + warns automatically; re-run with the flag\n# only if it suggests so. See \"Troubleshooting\" below for details.\n... | sudo bash -s -- --fix-blockers\n```\n\nAfter the script finishes:\n- Web UI is at `http://\u003cthis-host-ip\u003e/`, login `admin` / `password`\n  (**change it on first login** via *Settings → Account*).\n- `/opt/pitun/.env` was generated with a random `SECRET_KEY` and the\n  network block autodetected from your default-route interface:\n  `INTERFACE`, `LAN_CIDR`, `GATEWAY_IP` (the PiTun host's own LAN IP),\n  `VITE_API_BASE_URL`, `VITE_WS_BASE_URL`, `CORS_ORIGINS`. Verify with\n  `head -30 /opt/pitun/.env` before going to production; if anything\n  looks off, edit and `docker compose -f /opt/pitun/docker-compose.yml restart`.\n\n\u003e See [`install.sh --help`](install.sh) for the full option list.\n\n### Install — git clone path\n\nIf you want the source tree alongside the running stack (e.g. for\ndevelopment, or to apply patches before deploy), the classic path\nstill works:\n\n```bash\ngit clone https://github.com/DaveBugg/PiTun pitun\ncd pitun\n\n# Host bootstrap: installs Docker (if missing), xray-core, GeoIP/GeoSite,\n# system packages, kernel modules, sysctl tweaks, log rotation, daily\n# cleanup cron. Skip if you'd rather do it manually — see \"Manual install\"\n# below.\nsudo bash scripts/setup.sh\n\ncp .env.example .env\n# Edit .env — at minimum set SECRET_KEY, INTERFACE, LAN_CIDR,\n# GATEWAY_IP (the PiTun host's own LAN IP — what devices will use as\n# their default gateway). A random SECRET_KEY: openssl rand -hex 32\n#\n# Tip: instead of editing manually, run `sudo bash install.sh\n# --skip-host-prep` from the same checkout — it autodetects all the\n# network values from your default-route interface and writes them\n# into .env (only on first generation).\n\ndocker compose up -d --build\n```\n\nThe web UI listens on the host's LAN IP, port 80. Default login is\n`admin` / `password` — **change it on first run** via *Settings → Account*.\n\n### Manual install (without `setup.sh`)\n\nIf you'd rather wire the host yourself, here's the equivalent checklist.\nEverything below must be done **before** `docker compose up`:\n\n```bash\n# 1. System packages\nsudo apt update\nsudo apt install -y curl wget ca-certificates nftables iproute2 \\\n    net-tools iptables arp-scan dnsutils unzip jq cron\n\n# 2. Free UDP/5353 (PiTun's DNS port)\nsudo systemctl stop avahi-daemon avahi-daemon.socket || true\nsudo systemctl disable avahi-daemon avahi-daemon.socket || true\nsudo systemctl mask avahi-daemon || true\n\n# 3. Sysctl: IP forwarding + TPROXY loopback\nsudo tee /etc/sysctl.d/99-pitun.conf \u003c\u003c'EOF'\nnet.ipv4.ip_forward = 1\nnet.ipv6.conf.all.forwarding = 1\nnet.ipv4.conf.all.route_localnet = 1\nEOF\nsudo sysctl --system\n\n# 4. TPROXY kernel modules (load now + pin for next boot)\nsudo modprobe nft_tproxy xt_TPROXY\necho -e \"nft_tproxy\\nxt_TPROXY\" | sudo tee /etc/modules-load.d/pitun.conf\n\n# 5. Docker + Compose v2 (skip if already installed)\ncurl -fsSL https://get.docker.com | sudo sh\nsudo usermod -aG docker \"$USER\"   # then log out + back in\n\n# 6. GeoIP/GeoSite databases (bind-mounted RW into the backend container\n#    so the user can refresh them from the UI without an image rebuild).\n#    The xray binary itself is bundled inside the backend image as of\n#    v1.2.0 — no separate host install needed.\nsudo mkdir -p /usr/local/share/xray\nsudo curl -fsSL -o /usr/local/share/xray/geoip.dat   https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat\nsudo curl -fsSL -o /usr/local/share/xray/geosite.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat\n\n# 7. Static IP on the LAN interface (use NetworkManager, dhcpcd, or netplan\n#    depending on your distro; not scripted because the right tool varies).\n\n# 8. Now you can deploy\ncp .env.example .env \u0026\u0026 $EDITOR .env\ndocker compose up -d --build\n```\n\n\u003e **Why the geo databases are on the host, not inside the image.**\n\u003e `geoip.dat` and `geosite.dat` are refreshable from the UI\n\u003e (*GeoData → Update*). Keeping them as a host bind-mount means a\n\u003e single `curl` updates them in place — no image rebuild required.\n\u003e The xray binary itself, by contrast, is baked into the backend image\n\u003e as of v1.2.0 (used to be a host install). One less host-side\n\u003e prerequisite, version pinned to the release tag.\n\n### Pre-built images\n\nThe CI release workflow publishes loadable Docker tarballs (linux/amd64\nand linux/arm64) as GitHub Release assets. Useful for air-gapped /\nfactory-fresh RPi installs:\n\n```bash\n# On a machine with internet\ncurl -LO https://github.com/DaveBugg/PiTun/releases/download/vX.Y.Z/pitun-backend-vX.Y.Z-arm64.tar.gz\ncurl -LO https://github.com/DaveBugg/PiTun/releases/download/vX.Y.Z/pitun-frontend-vX.Y.Z.tar.gz\n\n# Transfer to the host, then:\ndocker load \u003c pitun-backend-vX.Y.Z-arm64.tar.gz\ntar -xzf pitun-frontend-vX.Y.Z.tar.gz -C frontend/dist/\ndocker compose up -d\n```\n\n### Setup scripts\n\nFor RPi-specific bootstrap (first boot, OS-level dependencies, network\nconfig) `scripts/` ships with helpers — see [scripts/README.md](scripts/README.md).\n\n## Server-side proxy install\n\nOnce your PiTun box is up and you've added a VPS under **Servers**, the\n**Install** button (rocket icon) lets you provision the upstream proxy\n*on the VPS itself* over SSH, with the install log streamed live in the\nmodal. Three protocols are supported:\n\n| Protocol | Topology | Where the credentials live |\n|---|---|---|\n| **NaiveProxy** | Single TLS tunnel per server | One `Node` row per deploy |\n| **WireGuard** | Multi-client (one server, N peers) | One `DeploymentClient` row per peer; export selected peers to `Node` rows on demand |\n| **x-ui** | Full 3x-ui / x-ui-pro panel (many inbounds + many clients per inbound) | Panel rows under `XuiServer`; each inbound's clients live on the panel itself, surfaced + cached by PiTun |\n\n\u003e The 443 slot — NaiveProxy and **x-ui-pro** can't coexist on one VPS\n\u003e (both want :443 with Let's Encrypt). Bare-mode x-ui (no domain →\n\u003e panel on a random high port) IS compatible with NaiveProxy. PiTun\n\u003e enforces this slot rule at deploy time with a clear error message.\n\n### NaiveProxy\n\nForm: domain (with an A-record pointing at the VPS), Let's Encrypt email,\noptional Caddy basic-auth user/password. Behind the scenes:\n\n1. PiTun uploads `scripts/setup-naive-server.sh` to the VPS.\n2. The script installs Caddy + the `caddyserver/forwardproxy` plugin via\n   `xcaddy`, fetches a TLS cert, writes a Caddyfile, enables the\n   service.\n3. On success the script emits `URI=naive+https://…` on its last line;\n   PiTun parses that, creates a `Node` row, and the deploy is done.\n\nA second deploy on the same server overwrites the existing\n`ServerDeployment` row; the linked `Node` is left intact (you can\nrecreate it from the deployment if it was deleted).\n\n### WireGuard\n\nForm: first-client name (defaults to `client1`), UDP port (default\n51820), DNS servers, AllowedIPs. The install script\n(`scripts/setup-wireguard-server.sh`) is sub-command driven:\n\n| Sub-command | Used by | What it does |\n|---|---|---|\n| `install` | Initial deploy | apt, sysctl, generate server keypair, write `wg0.conf`, enable `wg-quick@wg0`, add the first peer |\n| `add-client` | \"Add\" in the Clients modal | New keypair + `wg syncconf` reload (no tunnel restart) |\n| `remove-client` | \"Remove\" in the Clients modal | Strip peer + `wg syncconf` reload |\n| `list-clients` | \"Sync\" in the Clients modal | List current peer names + pubkeys |\n| `get-conf` | \"Download .conf\" | Re-print the cached INI for one client |\n\n**Clients are a separate layer.** The Servers page shows a `Users`\nicon next to any server with a WG deployment — that opens the\n**Clients** modal where you:\n\n- **Add** a peer (creates a new keypair on the VPS + caches the priv\n  key locally so we can render a `.conf` for download).\n- **Sync** to reconcile against the VPS — peers added manually on the\n  server show up; peers deleted on the server are flagged `orphan`\n  *but not auto-deleted from PiTun*, because the admin may have linked\n  them to Nodes.\n- **Download .conf** for QR-coding into a phone.\n- **Export to Node** to actually route traffic through this peer\n  (creates a `Node` row referencing the `DeploymentClient`; the Nodes\n  page renders \"from `\u003cserver name\u003e`\" alongside the row).\n- **Remove** to strip the peer from the server AND delete the\n  `DeploymentClient` row. Any Nodes that were exported from it stay\n  but get an `orphan` badge so the admin sees the upstream is gone.\n\nOne VPS can host clients used by multiple PiTun instances — each PiTun\nsees the peers it added itself + any it imported via Sync.\n\n### x-ui\n\nForm: optional domain (empty → bare-mode panel on a random high port,\nself-signed cert; non-empty → xui-pro stack with nginx + Let's Encrypt\non :443) and optional LE registration email (defaults to\n`admin@\u003capex-domain\u003e`). Behind the scenes\n(`scripts/setup-xui-server.sh`):\n\n1. Installs upstream **3x-ui v3.1.0** in `--non-interactive` mode.\n2. For xui-pro: also installs nginx + certbot + the\n   [`GFW4Fun/randomfakehtml`](https://github.com/GFW4Fun/randomfakehtml)\n   archive of fakesite templates, picks one at random, and wires the\n   reverse-proxy `externalProxy: 443` block.\n3. Generates fresh admin user/password + a Bearer API token + panel\n   basepath + sub-port, then emits `URI=xui://…` so PiTun creates the\n   `XuiServer` row + sets up the Bearer-authenticated client.\n4. Runs `cleanup-go.sh` to remove the Go SDK / build cache the\n   installer left behind (~2.5 GB on a 10 GB VPS).\n5. Applies the VPS optimisation profile (BBR + sysctl + swap +\n   ulimits + logrotate).\n\nOnce registered, every inbound + client on the panel becomes\nmanageable from the dedicated **Панели X-ui** page in the UI:\n6 wired-in inbound presets (VLESS+Reality / VLESS+xhttp+Reality /\nVLESS+WS+TLS / VLESS+xhttp+TLS / Trojan+gRPC / SOCKS5), per-client\nexport to `Node` rows (cache-backed, idempotent on re-export),\nmulti-layer healthcheck, cache↔panel sync that picks up clients\nadded directly via the panel UI, and (xui-pro only) fakesite\nrotation — random pick from the bundled archive or upload a custom\n`.zip`. Chain orchestration (see **Proxy Chains** below) glues two\nregistered panels into a two-hop VLESS+Reality tunnel.\n\n## Configuration\n\nAll runtime config goes through the web UI. The only settings that\nmust be set before first start, via `.env`:\n\n| Variable | Default | What |\n|---|---|---|\n| `SECRET_KEY` | `changeme-…` | JWT signing key — `openssl rand -hex 32` |\n| `INTERFACE` | `eth0` | LAN interface name on the host |\n| `LAN_CIDR` | `192.168.1.0/24` | Your LAN subnet (autodetected by `install.sh`) |\n| `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`. |\n| `BACKEND_PORT` | `8000` | Backend listen port (behind nginx) |\n| `TPROXY_PORT_TCP` | `7893` | TPROXY TCP listener |\n| `DNS_PORT` | `5353` | Internal DNS forwarder port |\n| `NAIVE_PORT_RANGE_START` | `20800` | Allocator range for naive sidecars |\n| `NAIVE_IMAGE` | `pitun-naive:latest` | Image tag built locally or loaded from release |\n\nFull annotated example: [`.env.example`](.env.example).\n\n\u003e **About `GATEWAY_IP`:** the variable name predates the LAN-gateway\n\u003e feature and refers to the PiTun host itself, not your home router.\n\u003e If the .env value disagrees with the actual interface IP, the backend\n\u003e auto-syncs the live IP into the database on the first `GET /settings`,\n\u003e so the UI always shows the truth. `LAN_CIDR` has the same runtime\n\u003e fallback as of 1.2.3.\n\n## Troubleshooting\n\n### `curl` hangs on every install / upgrade — stale kill-switch\n\n**Symptom.** You run `install.sh` and the very first download\n(`api.github.com/.../releases/...`) hangs forever (~75 s before\nTCP gives up). You can `ping 8.8.8.8` from the host but `curl\nhttps://api.github.com` doesn't return.\n\n**Cause.** PiTun's `kill_switch=true` mode installs an\n`inet pitun` nftables table + `ip rule fwmark 0x1 lookup 100`\npolicy route that TPROXYs all non-bypass traffic to xray on\n`127.0.0.1:7893`. If the backend dies with that protection still\nactive (crash, OOM, manual `docker compose down`), the rules stay\nin the kernel but xray isn't there to receive — every outbound\npacket drops silently. Even the installer.\n\n**Auto-detection.** Since v1.3.0-beta.3, `install.sh` checks for\nthis state at startup and prints a clear warning if it finds stale\nrules without a running `pitun-backend` container. If you see:\n\n```\n[WARN]  ════════════════════════════════════════════════════════════════════\n[WARN]    Detected stale kill-switch state on this host:\n[WARN]      * 'inet pitun' nftables table is present but backend is down\n[WARN]      * 'ip rule fwmark 0x1 lookup 100' policy route is present\n…\n```\n\n…re-run with the `--fix-blockers` flag (or `PITUN_FIX_BLOCKERS=1`):\n\n```bash\nsudo bash /tmp/pitun-install.sh --version v1.3.0 --fix-blockers\n```\n\nIt will flush the stale rules before any download, then proceed.\nThe backend will reinstall them at startup if needed.\n\n\u003e **When NOT to use `--fix-blockers`:** on a healthy install where\n\u003e `pitun-backend` is currently `Up`, kill-switch is doing its job\n\u003e and traffic flows fine through xray's bypass path. The installer\n\u003e auto-detects this case and leaves nftables alone — no flag\n\u003e needed. Adding the flag anyway just causes a brief LAN exposure\n\u003e while you re-stack.\n\n### `curl` hangs but no kill-switch state\n\nIf the install hangs on a fresh host (no PiTun previously installed)\nand `--fix-blockers` doesn't help, suspect IPv6. Some Debian 13 RPi\nimages and a number of VPS providers have advertised but unroutable\nIPv6 paths to GitHub. Since v1.3.0-beta.3 the installer defaults to\nIPv4 (`-4`) — but if you've passed `--ipv6` or set\n`PITUN_FORCE_IPV6=1`, drop those.\n\n### Backend container crash-loops with `bad marshal data` or `Segmentation fault`\n\nIf you upgraded to v1.3.0-beta.1 or .2 (via the original beta release\nartifacts) on an arm64 device — particularly **RPi 4 (Cortex-A72)** —\nthe backend container will crash-loop with one of:\n\n```\nValueError: bad marshal data (unknown type code)\n```\nor, worse, a silent `Segmentation fault (core dumped)` at\n`import uvicorn`. Root cause was a CI-side QEMU cross-build\nproducing arm64 wheels with corrupted bytecode caches.\n**Fixed in v1.3.0-beta.3** via Dockerfile (`PYTHONDONTWRITEBYTECODE=1`\n+ post-install `*.pyc` strip) and a switch to native arm64 runners\nin CI. Just re-run the installer with `--version v1.3.0-beta.3` (or\nlater); the new image loads cleanly on every Cortex-A72 / A76 we've\ntested.\n\n### Banner: `xray validation failed: (empty stderr)` after upgrade\n\n**Symptom.** Right after a fresh `install.sh` run the dashboard\ndisplays a red banner *\"Валидация конфигурации Xray не прошла /\nxray validation failed: (empty stderr)\"*. Backend logs show\n`xray process died unexpectedly (rc=-11)` (SIGSEGV, exit 139)\nand `Auto-restart aborted: config verification failed:` with no\nstderr — `xray -test -config …` segfaults before it can print\nanything.\n\n**Cause.** The bundled `xray` binary inside the freshly-loaded\nDocker image is bit-corrupted. The release tarball on GitHub is\nfine (verified by per-arch sha256 pinning at build time, since\nv1.3.0-beta.5), but `docker load \u003c tarball` writes layer files\nto local storage, and on a flaky SD card / SSD or with active\next4 metadata corruption, individual bytes flip silently. The\nbinary still has a valid ELF header (so `file` reports it as a\nplain executable), but executing instructions land on garbage\naddresses → segfault.\n\n\u003e **RPi 4 / 5 with a USB3-connected SSD?** The most common cause\n\u003e is the **UAS** USB-storage protocol corrupting bytes on heavy\n\u003e writes — not actual disk damage. Skip directly to\n\u003e [RPi 4 / 5 with USB-SATA / USB-NVMe SSD: disable UAS](#rpi-4--5-with-usb-sata--usb-nvme-ssd-disable-uas)\n\u003e for the one-time kernel-cmdline fix.\n\n**Auto-detection.** Since v1.3.0-beta.5, `install.sh` re-runs\n`docker load` up to 3 times and verifies the bundled xray's\nsha256 against a pinned digest after each attempt. If all three\nfail, the installer aborts with a pointer to this section\ninstead of leaving you with a non-bootable stack.\n\n**Recovery.**\n\n1. **Verify the diagnosis** — confirm the binary is corrupted\n   (and isn't just a config error):\n   ```bash\n   docker run --rm --entrypoint sha256sum pitun-backend:latest \\\n     /usr/local/bin/xray\n   ```\n   Compare to the pinned values in `backend/Dockerfile`\n   (`XRAY_SHA256_AMD64` / `_ARM64` / `_ARM`). If they don't\n   match, you've hit storage corruption.\n\n2. **Check the filesystem** — the most common root cause is\n   ext4 hash-tree (htree) corruption; one or more of:\n   ```bash\n   sudo dumpe2fs -h /dev/\u003crootdev\u003e | grep -E 'state|First error|Last error'\n   sudo dmesg -T | grep -iE 'ext4|mmc|sd|ata'\n   sudo smartctl -a /dev/\u003crootdev\u003e   # for SSD/NVMe\n   ```\n   `Filesystem state: clean with errors` plus recent\n   `EFSCORRUPTED` events is a strong signal.\n\n3. **Repair** — schedule an fsck on the next boot:\n   ```bash\n   sudo touch /forcefsck\n   sudo tune2fs -c 1 /dev/\u003crootdev\u003e\n   sudo reboot\n   ```\n   On a Pi, the boot will pause for fsck (1–10 min depending on\n   disk size and damage). After the reboot, `dumpe2fs` should\n   show `Filesystem state: clean` with no error timestamps.\n\n4. **Recover the data** — `install.sh` makes a pre-upgrade\n   SQLite snapshot at `/opt/pitun/data-backup-pre-vX.Y.Z-*.db`\n   on every run. Compose's `data:/app/data` bind mount also\n   keeps the live DB at `/opt/pitun/data/pitun.db`. Either is a\n   safe starting point if the running stack got into a bad\n   state.\n\n5. **Re-install** — wipe `/var/lib/docker/{overlay2,image,containers}/*`\n   while the daemon is stopped (orphaned layers from the\n   corrupted attempt won't auto-clean), then run `install.sh\n   --version vX.Y.Z` again. The retry loop in step 2 of the\n   installer should now pass.\n\n6. **If it keeps recurring** — the storage hardware is dying.\n   Back up `/opt/pitun/data` + `/opt/pitun/.env` to another\n   machine and swap the SD card / SSD before re-installing.\n   RPi 4 has no ECC RAM and SD cards are notoriously prone to\n   silent bit-rot; an M.2 SSD via USB3 is much more reliable.\n\n### RPi 4 / 5 with USB-SATA / USB-NVMe SSD: disable UAS\n\n**Who this affects.** Raspberry Pi 4 / 5 owners running root from an\nSSD (or plain HDD) connected via a USB3 → SATA / NVMe adapter — i.e.\n*not* SD card, *not* PCIe-direct NVMe HAT. Common culprits are\nASMedia bridges (`174c:1051`, `174c:1153`, `174c:1156`, `174c:55aa`)\nand JMicron (`152d:0578`, `152d:1561`, `152d:583*`). Check yours:\n\n```bash\nlsusb | grep -i -E 'asmedia|jmicron|realtek'\nlsusb -t   # look for \"Driver=uas\" — that's the trouble signal\n```\n\n**Why it breaks `install.sh`.** Most cheap USB-SATA bridges on Linux\ndefault to **UAS** (USB Attached SCSI) for speed, but several bridge\n+ Pi-firmware combinations silently flip bytes during heavy single-\nstream writes — exactly the workload of `docker load \u003c pitun-backend-\n*.tar.gz` (90 MB+). The result: bytes inside the bundled `xray`\nbinary get corrupted on disk, the binary segfaults on first use,\nand the dashboard greets you with **\"xray validation failed: (empty\nstderr)\"**. SMART says the SSD is fine; the bridge / driver is the\nculprit. Forcing the slower **BOT** (Bulk-Only Transport) protocol\ntrades ~20 % throughput for rock-solid integrity.\n\n**One-time fix (kernel cmdline quirk).** Replace `\u003cVID:PID\u003e` with\nyour bridge's IDs from `lsusb`. The example below is for an Argon\nONE M.2 + ASM1156 (`174c:1156`):\n\n```bash\n# 1. Find your bridge VID:PID\nlsusb -t                          # confirms \"Driver=uas\"\nlsusb | grep -i -E 'asmedia|jmicron|realtek'\n#   →  Bus 002 Device 002: ID 174c:1156 ASMedia Technology Inc. ...\nVIDPID=174c:1156                  # ← put YOUR VID:PID here\n\n# 2. Backup the current cmdline + insert the quirk\nsudo cp /boot/firmware/cmdline.txt /boot/firmware/cmdline.txt.bak\nsudo sed -i \"1s|^|usb-storage.quirks=${VIDPID}:u |\" /boot/firmware/cmdline.txt\ncat /boot/firmware/cmdline.txt    # verify\n\n# 3. Reboot\nsudo reboot\n\n# 4. After reboot — verify the driver flipped to usb-storage (not uas)\nlsusb -t                          # expect \"Driver=usb-storage\"\nsudo dmesg | grep -i 'uas is ignored'\n#   →  usb 2-2: UAS is ignored for this device, using usb-storage instead\n```\n\nAfter that, re-run `install.sh --version vX.Y.Z` — the new\nload-time sha verification (since v1.3.0-beta.5) will pass on the\nfirst attempt and any subsequent upgrade will be stable.\n\n**To revert** (if your bridge actually works fine on UAS and you\nwant the speed back), restore `/boot/firmware/cmdline.txt.bak` over\n`cmdline.txt` and reboot.\n\n\u003e The quirk only disables UAS for the matching VID:PID; other USB\n\u003e mass-storage devices on the same Pi (USB stick, second drive)\n\u003e are unaffected.\n\n## Uninstall\n\nTo completely remove PiTun from the host:\n\n```bash\n# Interactive — asks before host-level operations:\nsudo bash /opt/pitun/scripts/uninstall.sh\n\n# Headless re-image prep — wipes everything including host tweaks:\nsudo bash /opt/pitun/scripts/uninstall.sh --purge\n\n# Preview what would be removed, change nothing:\nsudo bash /opt/pitun/scripts/uninstall.sh --dry-run\n\n# Preserve DB + configs for a future reinstall:\nsudo bash /opt/pitun/scripts/uninstall.sh --yes --keep-data\n```\n\nThe uninstaller handles every installer permutation we ship —\nregistry-pulled images, locally-built (`--build`), offline bundles,\nthe dev compose stack, dynamic naive sidecars, hot-deploy backup\ndirs. It's idempotent (re-running on a cleaned host downgrades\nmissing artefacts to \"skip\") and safe by default (asks before\nnftables / sysctl / DNS / swap / host network changes).\n\nNotable flags:\n\n| Flag | Effect |\n|---|---|\n| `--dry-run` | Preview only — no changes. |\n| `-y` / `--yes` | Skip prompts on standard removals. |\n| `--purge` | Everything, including host network config. |\n| `--keep-data` | Preserve DB + configs (`data/` survives). |\n| `--keep-network` | Never touch network manager files. |\n| `--keep-xray` | Leave `/usr/local/bin/xray` + geo data alone. |\n\nSee [`scripts/README.md`](scripts/README.md#uninstall) for the\nfull list and per-flag rationale. **Phase 7 (host network)** is\nthe only HIGH-RISK step — it can break SSH if PiTun's IP was set\nvia the Settings UI. Open a second SSH session before confirming\nwhen remote.\n\n## Development\n\n```bash\n# Backend\ncd backend\npython -m venv .venv \u0026\u0026 source .venv/bin/activate\npip install -r requirements.txt -r requirements-dev.txt\npython -m uvicorn app.main:app --reload --port 8000\npython -m pytest tests/ -q\n\n# Frontend\ncd frontend\nnpm ci\nnpm run dev          # http://localhost:5173\nnpm run build        # tsc + vite (catches type errors)\nnpm run test:ci\nnpm run lint\n```\n\nThe full Docker stack lives in `docker-compose.yml`. For local UI work\nwithout RPi-specific bits (TPROXY, nftables) you can skip Docker — auth,\nnodes, routing rules and most of the UI work fine on macOS/Windows\nagainst a backend running on `localhost:8000`.\n\nSee [`CONTRIBUTING.md`](CONTRIBUTING.md) for PR conventions and code\nstyle.\n\n## Tech stack\n\n**Backend** — Python 3.11, FastAPI, SQLModel/SQLAlchemy, Alembic,\nPydantic v2, Uvicorn, httpx, aiohttp, aiosqlite, bcrypt, python-jose,\npsutil, docker-py, PyYAML.\n\n**Frontend** — React 19, TypeScript, Vite, Tailwind CSS 3, TanStack\nQuery (React Query) v5, Zustand, React Router 6, Recharts, Lucide\nReact, axios, clsx, tailwind-merge.\n\n**Infrastructure** — Docker + Compose, nginx (frontend), Tecnativa's\ndocker-socket-proxy (read-only Docker API access from the backend),\nnftables, systemd.\n\n**Testing** — pytest, Vitest, Testing Library.\n\n## Acknowledgements\n\nPiTun is glue code on top of mature, hard-to-replicate upstream\nprojects. Without them, none of this would exist:\n\n### Proxy / network core\n\n- **[XTLS/Xray-core](https://github.com/XTLS/Xray-core)** — the actual\n  proxy engine. PiTun manages an xray-core process, generates its\n  config, and talks to its gRPC API.\n- **[klzgrad/naiveproxy](https://github.com/klzgrad/naiveproxy)** —\n  Chromium-based HTTPS-tunnelling proxy used as a per-node sidecar.\n  PiTun's `docker/naive/` builds an image from upstream releases.\n- **[Caddy](https://caddyserver.com/)** with **[caddyserver/forwardproxy](https://github.com/caddyserver/forwardproxy)**\n  (klzgrad's fork) — recommended NaiveProxy server. `scripts/setup-naive-server.sh`\n  builds it via [`xcaddy`](https://github.com/caddyserver/xcaddy).\n- **[MHSanaei/3x-ui](https://github.com/MHSanaei/3x-ui)** — the upstream\n  x-ui panel (v3.1.0). PiTun's \"bare\" x-ui mode auto-installs it and\n  manages its inbounds/clients via the panel API.\n- **[GFW4Fun/x-ui-pro](https://github.com/GFW4Fun/x-ui-pro)** — domain\n  + nginx + LE fork of 3x-ui used in PiTun's \"xui-pro\" mode and as\n  relay/exit nodes in Proxy Chains.\n- **[GFW4Fun/randomfakehtml](https://github.com/GFW4Fun/randomfakehtml)**\n  — fakesite templates bundled into xui-pro installs, also driven by\n  the in-app \"rotate fakesite\" feature.\n- **[Loyalsoldier/v2ray-rules-dat](https://github.com/Loyalsoldier/v2ray-rules-dat)**\n  — GeoIP / GeoSite rule databases used by xray's `geoip:` / `geosite:`\n  matchers. PiTun pulls the latest `geoip.dat` and `geosite.dat` from\n  here.\n- **[MaxMind GeoLite2](https://www.maxmind.com/en/geolite2/)** —\n  GeoIP-MMDB lookups (optional, opt-in).\n- **[netfilter / nftables](https://www.netfilter.org/projects/nftables/)**\n  — kernel-side TPROXY interception.\n- **[arp-scan](https://github.com/royhills/arp-scan)** — LAN device\n  discovery.\n\n### Backend\n\n- **[FastAPI](https://github.com/tiangolo/fastapi)** — HTTP framework\n- **[SQLModel](https://github.com/tiangolo/sqlmodel)** + **[SQLAlchemy](https://www.sqlalchemy.org/)** — ORM\n- **[Pydantic](https://github.com/pydantic/pydantic)** — validation\n- **[Alembic](https://github.com/sqlalchemy/alembic)** — migrations\n- **[Uvicorn](https://github.com/encode/uvicorn)** — ASGI server\n- **[httpx](https://github.com/encode/httpx)** + **[aiohttp](https://github.com/aio-libs/aiohttp)** — HTTP clients\n- **[asyncssh](https://github.com/ronf/asyncssh)** — async SSH client used for VPS auto-deploy and remote diagnostics\n- **[websockets](https://github.com/python-websockets/websockets)** — install-log streaming\n- **[aiosqlite](https://github.com/omnilib/aiosqlite)** — async SQLite\n- **[python-jose](https://github.com/mpdavis/python-jose)** + **[bcrypt](https://github.com/pyca/bcrypt/)** — auth\n- **[psutil](https://github.com/giampaolo/psutil)** — host metrics\n- **[docker-py](https://github.com/docker/docker-py)** — Docker API client (Naive sidecar lifecycle)\n- **[PyYAML](https://pyyaml.org/)** — Clash YAML import\n\n### Frontend\n\n- **[React](https://react.dev/)**, **[Vite](https://vitejs.dev/)**,\n  **[TypeScript](https://www.typescriptlang.org/)**\n- **[Tailwind CSS](https://tailwindcss.com/)** — styling\n- **[TanStack Query](https://tanstack.com/query)** — server state\n- **[Zustand](https://github.com/pmndrs/zustand)** — UI state\n- **[React Router](https://reactrouter.com/)** — routing\n- **[Recharts](https://recharts.org/)** — metrics charts\n- **[Lucide](https://lucide.dev/)** — icons\n- **[axios](https://github.com/axios/axios)** — HTTP client\n- **[Vitest](https://vitest.dev/)** + **[Testing Library](https://testing-library.com/)** — tests\n\n### Infrastructure\n\n- **[Docker](https://www.docker.com/)** + **[Compose](https://docs.docker.com/compose/)**\n- **[nginx](https://nginx.org/)** — frontend serving + WebSocket proxy\n- **[Tecnativa/docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy)**\n  — locked-down Docker API access for the backend\n\nPiTun's import format compatibility (V2RayN / Shadowrocket / Clash JSON)\nis inspired by the formats of those projects — no code is borrowed.\n\n## Contributing\n\nBug reports and PRs welcome. See [`CONTRIBUTING.md`](CONTRIBUTING.md)\nfor code style, PR conventions, and what to keep out of the repo.\n\n## License\n\n[BSD 3-Clause](LICENSE) © PiTun contributors\n\n---\n\n\u003e **Disclaimer.** PiTun is a network management tool. You are responsible\n\u003e for complying with the laws of your jurisdiction and the terms of\n\u003e service of any upstream provider you use it with. The maintainers\n\u003e provide no warranty and accept no liability for misuse.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavebugg%2Fpitun","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdavebugg%2Fpitun","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavebugg%2Fpitun/lists"}