├── packaging ├── snap │ ├── .gitignore │ ├── snapcraft.yaml │ └── README.md ├── debian │ ├── install │ ├── changelog │ ├── control │ └── copyright ├── .gitignore ├── build-snap.sh ├── calculate-checksums.sh ├── homebrew │ └── floo.rb ├── aur │ └── PKGBUILD ├── setup-apt-repo.sh ├── build-deb.sh └── README.md ├── website ├── vite.config.js ├── src │ ├── main.jsx │ ├── App.jsx │ ├── components │ │ ├── Features.css │ │ ├── Footer.css │ │ ├── Footer.jsx │ │ ├── Features.jsx │ │ ├── GitHub.css │ │ ├── Performance.css │ │ ├── Installation.css │ │ ├── Performance.jsx │ │ ├── Comparison.css │ │ ├── Hero.jsx │ │ ├── Comparison.jsx │ │ ├── GitHub.jsx │ │ ├── Installation.jsx │ │ └── Hero.css │ ├── index.css │ └── App.css ├── .gitignore ├── package.json ├── index.html └── README.md ├── examples ├── through-corporate-proxy │ ├── floos.toml │ ├── flooc.toml │ └── README.md ├── multi-client-loadbalancing │ ├── floos.toml │ ├── flooc-site-a.toml │ ├── flooc-site-b.toml │ └── README.md ├── access-cloud-database │ ├── floos.toml │ ├── flooc.toml │ └── README.md ├── reverse-forwarding-emby │ ├── floos.toml │ ├── flooc.toml │ └── README.md ├── expose-multiple-services │ ├── floos.toml │ ├── flooc.toml │ └── README.md ├── expose-home-server │ ├── floos.toml │ ├── flooc.toml │ └── README.md └── README.md ├── .gitignore ├── configs ├── flooc.minimal.toml ├── floos.minimal.toml ├── README.md ├── floos.example.toml └── flooc.example.toml ├── .github └── workflows │ ├── deploy-website.yml │ ├── ci.yml │ ├── publish-snap.yml │ ├── nightly.yml │ ├── publish-apt.yml │ └── release.yml ├── src ├── diagnostics.zig ├── net_compat.zig ├── udp_client.zig ├── udp_session.zig ├── udp_server.zig └── protocol.zig └── CHANGELOG.md /packaging/snap/.gitignore: -------------------------------------------------------------------------------- 1 | # Snap build artifacts 2 | *.snap 3 | parts/ 4 | stage/ 5 | prime/ 6 | *.snapcraft 7 | 8 | # Snapcraft state 9 | .snapcraft/ 10 | __pycache__/ 11 | -------------------------------------------------------------------------------- /packaging/debian/install: -------------------------------------------------------------------------------- 1 | flooc usr/bin 2 | floos usr/bin 3 | README.md usr/share/doc/floo 4 | flooc.toml.example usr/share/doc/floo/examples 5 | floos.toml.example usr/share/doc/floo/examples 6 | -------------------------------------------------------------------------------- /website/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | base: '/floo/', 7 | }) 8 | -------------------------------------------------------------------------------- /packaging/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore downloaded release artifacts 2 | *.tar.gz 3 | *.zip 4 | *.deb 5 | 6 | # Ignore build directories 7 | build-deb/ 8 | apt-repo/ 9 | 10 | # Ignore temporary checksum files 11 | checksums.txt 12 | -------------------------------------------------------------------------------- /website/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /examples/through-corporate-proxy/floos.toml: -------------------------------------------------------------------------------- 1 | bind = "0.0.0.0" 2 | port = 8443 3 | cipher = "aes256gcm" 4 | psk = "corp-proxy-psk" 5 | token = "corp-proxy-token" 6 | 7 | [services] 8 | intranet = "10.0.0.10:443" 9 | 10 | [advanced] 11 | tcp_keepalive = true 12 | heartbeat_interval_seconds = 30 13 | heartbeat_timeout_seconds = 45 14 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Build output 5 | dist 6 | 7 | # Logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | pnpm-debug.log* 13 | 14 | # Editor directories and files 15 | .vscode 16 | .idea 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | 23 | # OS files 24 | .DS_Store 25 | Thumbs.db 26 | -------------------------------------------------------------------------------- /examples/multi-client-loadbalancing/floos.toml: -------------------------------------------------------------------------------- 1 | bind = "0.0.0.0" 2 | port = 8443 3 | cipher = "aes256gcm" 4 | psk = "balancer-psk" 5 | token = "balancer-token" 6 | 7 | [reverse_services] 8 | webcluster = "0.0.0.0:8080" 9 | 10 | [advanced] 11 | pin_threads = true 12 | io_batch_bytes = 131072 13 | socket_buffer_size = 8388608 14 | heartbeat_interval_seconds = 20 15 | heartbeat_timeout_seconds = 35 16 | -------------------------------------------------------------------------------- /packaging/debian/changelog: -------------------------------------------------------------------------------- 1 | floo (0.1.2-1) stable; urgency=medium 2 | 3 | * Initial release 4 | * High-performance tunneling with 29.4 Gbps throughput 5 | * Zero dependencies, 671 KB total binary size 6 | * Noise XX + PSK authentication with 5 AEAD ciphers 7 | * Built-in diagnostics and hot config reload 8 | 9 | -- Your Name Thu, 07 Nov 2024 00:00:00 +0000 10 | -------------------------------------------------------------------------------- /examples/multi-client-loadbalancing/flooc-site-a.toml: -------------------------------------------------------------------------------- 1 | server = "YOUR_SERVER_IP:8443" 2 | cipher = "aes256gcm" 3 | psk = "balancer-psk" 4 | token = "balancer-token" 5 | 6 | [reverse_services] 7 | webcluster = "10.0.1.10:8080" # Site A origin 8 | 9 | [advanced] 10 | num_tunnels = 0 # Auto-match CPU cores 11 | pin_threads = true 12 | io_batch_bytes = 131072 13 | socket_buffer_size = 8388608 14 | reconnect_enabled = true 15 | -------------------------------------------------------------------------------- /examples/multi-client-loadbalancing/flooc-site-b.toml: -------------------------------------------------------------------------------- 1 | server = "YOUR_SERVER_IP:8443" 2 | cipher = "aes256gcm" 3 | psk = "balancer-psk" 4 | token = "balancer-token" 5 | 6 | [reverse_services] 7 | webcluster = "10.0.2.10:8080" # Site B origin 8 | 9 | [advanced] 10 | num_tunnels = 0 # Auto-match CPU cores 11 | pin_threads = true 12 | io_batch_bytes = 131072 13 | socket_buffer_size = 8388608 14 | reconnect_enabled = true 15 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "floo-website", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0" 13 | }, 14 | "devDependencies": { 15 | "@vitejs/plugin-react": "^4.2.0", 16 | "vite": "^5.0.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/access-cloud-database/floos.toml: -------------------------------------------------------------------------------- 1 | bind = "0.0.0.0" 2 | port = 8443 3 | cipher = "aes256gcm" 4 | psk = "your-secure-database-key" 5 | token = "database-access-token" 6 | 7 | [services] 8 | postgres = "127.0.0.1:5432" 9 | 10 | [advanced] 11 | tcp_keepalive = true 12 | pin_threads = true 13 | io_batch_bytes = 131072 14 | socket_buffer_size = 8388608 15 | heartbeat_interval_seconds = 60 16 | heartbeat_timeout_seconds = 80 17 | -------------------------------------------------------------------------------- /examples/access-cloud-database/flooc.toml: -------------------------------------------------------------------------------- 1 | server = "your-database-server.com:8443" 2 | cipher = "aes256gcm" 3 | psk = "your-secure-database-key" 4 | token = "database-access-token" 5 | 6 | [services] 7 | postgres = "127.0.0.1:5432" 8 | 9 | [advanced] 10 | num_tunnels = 0 # Auto-match CPU cores 11 | pin_threads = true 12 | io_batch_bytes = 131072 13 | socket_buffer_size = 8388608 14 | reconnect_enabled = true 15 | heartbeat_timeout_seconds = 120 16 | -------------------------------------------------------------------------------- /examples/reverse-forwarding-emby/floos.toml: -------------------------------------------------------------------------------- 1 | bind = "0.0.0.0" 2 | port = 8443 3 | cipher = "aes256gcm" 4 | psk = "emby-reverse-forward-secret-2024" 5 | token = "emby-access-token" 6 | 7 | [reverse_services] 8 | emby = "0.0.0.0:8096" 9 | 10 | [advanced] 11 | tcp_nodelay = true 12 | tcp_keepalive = true 13 | tcp_keepalive_idle = 30 14 | tcp_keepalive_interval = 10 15 | tcp_keepalive_count = 3 16 | socket_buffer_size = 524288 17 | heartbeat_interval_seconds = 30 18 | heartbeat_timeout_seconds = 45 19 | -------------------------------------------------------------------------------- /examples/through-corporate-proxy/flooc.toml: -------------------------------------------------------------------------------- 1 | server = "your-vps-ip:8443" 2 | cipher = "aes256gcm" 3 | psk = "corp-proxy-psk" 4 | token = "corp-proxy-token" 5 | 6 | [services] 7 | intranet = "127.0.0.1:9443" # Local port for internal HTTPS 8 | 9 | [advanced] 10 | num_tunnels = 1 # Stay within proxy limits (set 0 for auto scaling) 11 | pin_threads = true 12 | io_batch_bytes = 131072 13 | socket_buffer_size = 4194304 14 | reconnect_enabled = true 15 | proxy_url = "socks5://proxy.corp.local:1080" 16 | -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Floo - High-Performance Tunneling in Zig 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Zig build artifacts 2 | zig-out/ 3 | zig-cache/ 4 | .zig-cache/ 5 | 6 | # Build directories 7 | build/ 8 | dist/ 9 | 10 | # Editor directories and files 11 | .vscode/ 12 | .idea/ 13 | *.swp 14 | *.swo 15 | *~ 16 | 17 | # OS files 18 | .DS_Store 19 | Thumbs.db 20 | 21 | # Test and log files 22 | *.log 23 | 24 | # User-specific configs (keep out of repo) 25 | *-emby.toml 26 | *-minimal.toml 27 | EMBY_*.md 28 | EMBY_*.txt 29 | QUICKSTART_EMBY.md 30 | 31 | # Test configurations 32 | test-*.toml 33 | 34 | # Build artifacts 35 | build-deb/ 36 | .DS_Store 37 | -------------------------------------------------------------------------------- /configs/flooc.minimal.toml: -------------------------------------------------------------------------------- 1 | # Minimal Floo Client Configuration 2 | # ================================== 3 | # Credentials must match your server! 4 | 5 | server = "your-server:8443" 6 | cipher = "aes256gcm" 7 | psk = "REPLACE_ME" # ⚠️ Must match server PSK! 8 | token = "REPLACE_ME" # ⚠️ Must match server token! 9 | 10 | # Expose local service through server's reverse tunnel 11 | [reverse_services] 12 | webapp = "127.0.0.1:8080" 13 | 14 | # Or access remote services locally 15 | # [services] 16 | # database = "127.0.0.1:5432" 17 | 18 | [advanced] 19 | num_tunnels = 0 20 | pin_threads = true 21 | -------------------------------------------------------------------------------- /examples/expose-multiple-services/floos.toml: -------------------------------------------------------------------------------- 1 | bind = "0.0.0.0" 2 | port = 8443 3 | cipher = "aegis128l" 4 | psk = "multi-service-psk-2024" 5 | token = "multi-service-token-2024" 6 | 7 | [reverse_services] 8 | emby = "0.0.0.0:8096" 9 | emby.token = "media-only-token" 10 | ssh = "0.0.0.0:2222" 11 | ssh.token = "ssh-only-token" 12 | 13 | [advanced] 14 | tcp_nodelay = true 15 | tcp_keepalive = true 16 | tcp_keepalive_idle = 45 17 | tcp_keepalive_interval = 10 18 | tcp_keepalive_count = 3 19 | socket_buffer_size = 8388608 20 | pin_threads = true 21 | io_batch_bytes = 131072 22 | heartbeat_interval_seconds = 30 23 | heartbeat_timeout_seconds = 50 24 | -------------------------------------------------------------------------------- /examples/reverse-forwarding-emby/flooc.toml: -------------------------------------------------------------------------------- 1 | server = "your-vps-ip:8443" 2 | cipher = "aes256gcm" 3 | psk = "emby-reverse-forward-secret-2024" 4 | token = "emby-access-token" 5 | 6 | [reverse_services] 7 | emby = "127.0.0.1:8096" 8 | 9 | [advanced] 10 | num_tunnels = 0 # Auto-match CPU cores 11 | pin_threads = true 12 | io_batch_bytes = 131072 13 | tcp_nodelay = true 14 | tcp_keepalive = true 15 | tcp_keepalive_idle = 30 16 | tcp_keepalive_interval = 10 17 | tcp_keepalive_count = 3 18 | socket_buffer_size = 8388608 19 | heartbeat_timeout_seconds = 60 20 | reconnect_enabled = true 21 | reconnect_initial_delay_ms = 1000 22 | reconnect_max_delay_ms = 30000 23 | reconnect_backoff_multiplier = 2 24 | -------------------------------------------------------------------------------- /website/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import './App.css' 3 | import Hero from './components/Hero' 4 | import Features from './components/Features' 5 | import Performance from './components/Performance' 6 | import Comparison from './components/Comparison' 7 | import Installation from './components/Installation' 8 | import GitHub from './components/GitHub' 9 | import Footer from './components/Footer' 10 | 11 | function App() { 12 | return ( 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 | ) 23 | } 24 | 25 | export default App 26 | -------------------------------------------------------------------------------- /configs/floos.minimal.toml: -------------------------------------------------------------------------------- 1 | # Minimal Floo Server Configuration 2 | # ================================== 3 | # Generate credentials with: 4 | # openssl rand -base64 32 (for psk) 5 | # openssl rand -base64 24 (for token) 6 | 7 | bind = "0.0.0.0" 8 | port = 8443 9 | cipher = "aes256gcm" 10 | psk = "REPLACE_ME" # ⚠️ Replace or server will refuse to start! 11 | token = "REPLACE_ME" # ⚠️ Replace or server will refuse to start! 12 | 13 | # Accept reverse tunnel from clients and expose publicly 14 | [reverse_services] 15 | webapp = "0.0.0.0:80" 16 | 17 | # Or allow clients to reach internal services through tunnel 18 | # [services] 19 | # database = "127.0.0.1:5432" 20 | 21 | [advanced] 22 | pin_threads = true 23 | socket_buffer_size = 8388608 24 | -------------------------------------------------------------------------------- /examples/expose-multiple-services/flooc.toml: -------------------------------------------------------------------------------- 1 | server = "YOUR_SERVER_IP:8443" 2 | cipher = "aegis128l" 3 | psk = "multi-service-psk-2024" 4 | token = "multi-service-token-2024" 5 | 6 | [reverse_services] 7 | emby = "127.0.0.1:8096" 8 | ssh = "127.0.0.1:22" 9 | ssh.token = "ssh-only-token" 10 | 11 | [advanced] 12 | num_tunnels = 0 # Auto-match CPU cores 13 | pin_threads = true 14 | io_batch_bytes = 131072 15 | tcp_nodelay = true 16 | tcp_keepalive = true 17 | tcp_keepalive_idle = 30 18 | tcp_keepalive_interval = 10 19 | tcp_keepalive_count = 3 20 | socket_buffer_size = 8388608 21 | heartbeat_timeout_seconds = 60 22 | reconnect_enabled = true 23 | reconnect_initial_delay_ms = 1000 24 | reconnect_max_delay_ms = 30000 25 | reconnect_backoff_multiplier = 2 26 | -------------------------------------------------------------------------------- /examples/multi-client-loadbalancing/README.md: -------------------------------------------------------------------------------- 1 | # Multi-Client Load Balancing 2 | 3 | Run multiple flooc instances that advertise the **same reverse service name** to 4 | share incoming traffic. floos round-robins new connections across the tunnels. 5 | 6 | ## Files 7 | 8 | - `floos.toml` – publishes `webcluster` on port 8080. 9 | - `flooc-site-a.toml` – forwards traffic to Site A origin (`10.0.1.10:8080`). 10 | - `flooc-site-b.toml` – forwards traffic to Site B origin (`10.0.2.10:8080`). 11 | 12 | ## How to use 13 | 14 | 1. Start floos on the public server. 15 | 2. Start flooc on Site A and Site B (can be different geographic regions). 16 | 3. Each client leaves `num_tunnels = 0`, so Floo opens one tunnel per CPU core 17 | (set an explicit value if you need to cap fan-out). 18 | 4. When users hit `http://YOUR_SERVER_IP:8080`, Floo rotates connections between 19 | Site A and Site B tunnels. 20 | 21 | Add or remove clients at will—just keep the `[reverse_services]` name identical. 22 | -------------------------------------------------------------------------------- /examples/through-corporate-proxy/README.md: -------------------------------------------------------------------------------- 1 | # Through a Corporate Proxy 2 | 3 | flooc can dial floos via SOCKS5 or HTTP CONNECT proxies. Point `advanced.proxy_url` 4 | to the proxy endpoint and Floo will wrap the tunnel inside that connection. 5 | 6 | ## Example workflow 7 | 8 | 1. Update `flooc.toml` 9 | - Set `server` to your public floos host 10 | - Set `proxy_url = "socks5://proxy.corp.local:1080"` (or `http://user:pass@proxy:8080`) 11 | - Choose a local port under `[services]` so you can run `curl https://localhost:9443` 12 | 2. Start flooc from inside the restricted network: 13 | 14 | ```bash 15 | ./flooc flooc.toml 16 | ``` 17 | 18 | 3. flooc establishes a proxy tunnel, performs the Noise handshake, and exposes 19 | the remote service on `127.0.0.1:9443`. 20 | 21 | 4. Use your normal tooling against localhost: 22 | 23 | ```bash 24 | curl -k https://127.0.0.1:9443/internal-status 25 | ``` 26 | 27 | The proxy never sees the decrypted application traffic—only the Floo TCP stream. 28 | -------------------------------------------------------------------------------- /examples/expose-home-server/floos.toml: -------------------------------------------------------------------------------- 1 | # Floo Server Config - Expose Home Media Server Example 2 | # ======================================================= 3 | # Run this on your PUBLIC VPS to accept tunnel connections 4 | 5 | bind = "0.0.0.0" 6 | port = 8443 7 | cipher = "aes256gcm" 8 | 9 | # ⚠️ REPLACE THESE! Generate with: openssl rand -base64 32/24 10 | psk = "REPLACE_WITH_YOUR_PSK_FROM_OPENSSL" 11 | token = "REPLACE_WITH_YOUR_TOKEN_FROM_OPENSSL" 12 | 13 | [reverse_services] 14 | # Accept connections from home and expose on port 8096 15 | # Friends access: http://YOUR_VPS_IP:8096 16 | jellyfin = "0.0.0.0:8096" 17 | 18 | [advanced] 19 | # Optimized for media streaming 20 | tcp_nodelay = true 21 | tcp_keepalive = true 22 | tcp_keepalive_idle = 45 23 | tcp_keepalive_interval = 10 24 | tcp_keepalive_count = 3 25 | socket_buffer_size = 8388608 # 8MB buffers for 4K streaming 26 | pin_threads = true 27 | io_batch_bytes = 131072 28 | heartbeat_interval_seconds = 30 29 | heartbeat_timeout_seconds = 60 30 | -------------------------------------------------------------------------------- /packaging/debian/control: -------------------------------------------------------------------------------- 1 | Source: floo 2 | Section: net 3 | Priority: optional 4 | Maintainer: Your Name 5 | Build-Depends: debhelper (>= 10) 6 | Standards-Version: 4.5.0 7 | Homepage: https://github.com/YUX/floo 8 | Vcs-Git: https://github.com/YUX/floo.git 9 | Vcs-Browser: https://github.com/YUX/floo 10 | 11 | Package: floo 12 | Architecture: amd64 arm64 13 | Depends: ${shlibs:Depends}, ${misc:Depends} 14 | Description: Secure, high-performance tunneling in Zig 15 | Floo is a high-throughput tunneling solution written in Zig with zero 16 | dependencies. It provides secure tunneling using Noise XX protocol with 17 | multiple AEAD ciphers. 18 | . 19 | Features: 20 | - 29.4 Gbps throughput with AEGIS-128L cipher 21 | - Zero runtime dependencies 22 | - Reverse and forward tunneling modes 23 | - SOCKS5 and HTTP CONNECT proxy support 24 | - Config changes applied on restart (SIGHUP reload temporarily disabled) 25 | - Built-in diagnostics (--doctor, --ping) 26 | - Automatic reconnection with exponential backoff 27 | . 28 | This package includes both the client (flooc) and server (floos) binaries. 29 | -------------------------------------------------------------------------------- /examples/access-cloud-database/README.md: -------------------------------------------------------------------------------- 1 | # Access a Cloud Database Securely 2 | 3 | Forward PostgreSQL/MySQL/Redis traffic through Floo so the database only listens 4 | on localhost inside your VPS. 5 | 6 | ``` 7 | Laptop (flooc) ──▶ localhost:5432 ──▶ encrypted tunnel ──▶ floos ──▶ DB :5432 8 | ``` 9 | 10 | ## Files in this folder 11 | 12 | - `floos.toml` – binds to `0.0.0.0:8443` and exposes `[services].postgres = 13 | "127.0.0.1:5432"` 14 | - `flooc.toml` – listens on `127.0.0.1:5432` so local tools can connect 15 | 16 | ## Quick start 17 | 18 | ```bash 19 | # On the VPS that hosts the database 20 | ./floos floos.toml 21 | 22 | # On your laptop/workstation 23 | ./flooc flooc.toml 24 | psql -h 127.0.0.1 -p 5432 -U dbuser dbname 25 | ``` 26 | 27 | ## Customisation ideas 28 | 29 | - Change `[services]` entries to point at different targets (e.g. Redis on 6379, 30 | MongoDB on 27017). Run multiple flooc instances if you want separate local 31 | ports. 32 | - Leave `num_tunnels = 0` so Floo auto-matches CPU cores; only set a manual value 33 | when you need to cap or boost concurrency for specific workloads. 34 | connections. 35 | - Increase `socket_buffer_size` for large result sets over high-latency links. 36 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Floo Examples 2 | 3 | Each subdirectory provides ready-to-run `floos.toml`/`flooc.toml` pairs for a 4 | specific deployment pattern. 5 | 6 | | Example | Mode | Highlights | 7 | |---------|------|------------| 8 | | [`access-cloud-database/`](access-cloud-database/) | Forward | Keep databases private while admins connect over localhost | 9 | | [`expose-home-server/`](expose-home-server/) | Reverse | Publish Jellyfin/Emby from home through a VPS | 10 | | [`expose-multiple-services/`](expose-multiple-services/) | Reverse | Serve media + SSH simultaneously with per-service tokens | 11 | | [`multi-client-loadbalancing/`](multi-client-loadbalancing/) | Reverse | Run multiple flooc instances for round-robin load sharing | 12 | | [`reverse-forwarding-emby/`](reverse-forwarding-emby/) | Reverse | Minimal example focused on media streaming | 13 | | [`through-corporate-proxy/`](through-corporate-proxy/) | Forward | Dial through SOCKS5/HTTP proxies while keeping traffic encrypted | 14 | 15 | All configs follow the same structure implemented in `src/config.zig`. Copy the 16 | pair that matches your use case, update secrets and IPs, then run `./floos` on 17 | the server and `./flooc` on the client. Use `--doctor` before going live. 18 | -------------------------------------------------------------------------------- /packaging/build-snap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Build Snap package locally 4 | # Usage: ./build-snap.sh [VERSION] 5 | # Example: ./build-snap.sh 0.1.2 6 | 7 | set -e 8 | 9 | VERSION=${1:-$(grep -oP '\.version = "\K[^"]+' ../build.zig.zon)} 10 | 11 | echo "Building Snap package for Floo v${VERSION}..." 12 | echo "" 13 | 14 | # Check for snapcraft 15 | if ! command -v snapcraft &> /dev/null; then 16 | echo "Error: snapcraft not found." 17 | echo "Install with: sudo snap install snapcraft --classic" 18 | exit 1 19 | fi 20 | 21 | # Update version in snapcraft.yaml 22 | cd snap 23 | sed -i.bak "s/^version:.*/version: '${VERSION}'/" snapcraft.yaml 24 | rm -f snapcraft.yaml.bak 25 | 26 | # Build snap 27 | echo "Building snap..." 28 | snapcraft 29 | 30 | SNAP_FILE="floo_${VERSION}_$(dpkg --print-architecture).snap" 31 | 32 | echo "" 33 | echo "=== Snap built successfully ===" 34 | echo "File: packaging/snap/${SNAP_FILE}" 35 | echo "" 36 | echo "To install locally:" 37 | echo " sudo snap install ${SNAP_FILE} --dangerous" 38 | echo "" 39 | echo "To test:" 40 | echo " floo.flooc --version" 41 | echo " floo.floos --version" 42 | echo "" 43 | echo "To publish to Snap Store:" 44 | echo " snapcraft upload ${SNAP_FILE} --release=stable" 45 | -------------------------------------------------------------------------------- /packaging/debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: floo 3 | Upstream-Contact: Your Name 4 | Source: https://github.com/YUX/floo 5 | 6 | Files: * 7 | Copyright: 2024 Your Name 8 | License: MIT 9 | 10 | License: MIT 11 | Permission is hereby granted, free of charge, to any person obtaining a 12 | copy of this software and associated documentation files (the "Software"), 13 | to deal in the Software without restriction, including without limitation 14 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | and/or sell copies of the Software, and to permit persons to whom the 16 | Software is furnished to do so, subject to the following conditions: 17 | . 18 | The above copyright notice and this permission notice shall be included 19 | in all copies or substantial portions of the Software. 20 | . 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 24 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | DEALINGS IN THE SOFTWARE. 28 | -------------------------------------------------------------------------------- /examples/expose-home-server/flooc.toml: -------------------------------------------------------------------------------- 1 | # Floo Client Config - Expose Home Media Server Example 2 | # ======================================================= 3 | # Run this on your HOME MACHINE where Jellyfin/Plex runs 4 | 5 | server = "YOUR_VPS_IP_OR_HOSTNAME:8443" # ← Replace with your VPS address 6 | cipher = "aes256gcm" 7 | 8 | # ⚠️ MUST MATCH SERVER! Use same credentials from floos.toml 9 | psk = "REPLACE_WITH_SAME_PSK_AS_SERVER" 10 | token = "REPLACE_WITH_SAME_TOKEN_AS_SERVER" 11 | 12 | [reverse_services] 13 | # Expose your local Jellyfin instance 14 | # Make sure Jellyfin is running on this port locally! 15 | jellyfin = "127.0.0.1:8096" 16 | 17 | [advanced] 18 | # Match tunnel fan-out to your CPU cores (override with >0 if needed) 19 | num_tunnels = 0 20 | 21 | # Keep tunnel threads on dedicated cores (Linux/Unix) 22 | pin_threads = true 23 | 24 | # Per-stream buffering for smoother video 25 | io_batch_bytes = 131072 26 | 27 | # TCP optimization for streaming 28 | tcp_nodelay = true 29 | tcp_keepalive = true 30 | tcp_keepalive_idle = 45 31 | tcp_keepalive_interval = 10 32 | tcp_keepalive_count = 3 33 | 34 | # Larger buffers for smooth 4K streaming 35 | socket_buffer_size = 8388608 # 8MB 36 | 37 | # Auto-reconnect if internet drops 38 | reconnect_enabled = true 39 | reconnect_initial_delay_ms = 1000 40 | reconnect_max_delay_ms = 30000 41 | reconnect_backoff_multiplier = 2 42 | 43 | # Heartbeat keeps connection alive 44 | heartbeat_timeout_seconds = 60 45 | -------------------------------------------------------------------------------- /packaging/calculate-checksums.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Calculate SHA256 checksums for Floo release artifacts 4 | # Usage: ./calculate-checksums.sh VERSION 5 | # Example: ./calculate-checksums.sh v0.1.2 6 | 7 | set -e 8 | 9 | if [ -z "$1" ]; then 10 | echo "Usage: $0 VERSION" 11 | echo "Example: $0 v0.1.2" 12 | exit 1 13 | fi 14 | 15 | VERSION=$1 16 | BASE_URL="https://github.com/YUX/floo/releases/download/${VERSION}" 17 | 18 | # Create temp directory 19 | TEMP_DIR=$(mktemp -d) 20 | cd "$TEMP_DIR" 21 | 22 | echo "Downloading release artifacts for $VERSION..." 23 | echo "" 24 | 25 | ARTIFACTS=( 26 | floo-x86_64-linux-gnu.tar.gz 27 | floo-x86_64-linux-gnu-haswell.tar.gz 28 | floo-x86_64-linux-musl.tar.gz 29 | floo-aarch64-linux-gnu.tar.gz 30 | floo-aarch64-linux-gnu-neoverse-n1.tar.gz 31 | floo-aarch64-linux-gnu-rpi4.tar.gz 32 | floo-x86_64-macos.tar.gz 33 | floo-x86_64-macos-haswell.tar.gz 34 | floo-aarch64-macos-m1.tar.gz 35 | ) 36 | 37 | for artifact in "${ARTIFACTS[@]}"; do 38 | echo "==> ${artifact}" 39 | wget -q "${BASE_URL}/${artifact}" 40 | if command -v shasum &> /dev/null; then 41 | shasum -a 256 "${artifact}" 42 | else 43 | sha256sum "${artifact}" 44 | fi 45 | echo "" 46 | done 47 | 48 | # Cleanup 49 | cd - 50 | rm -rf "$TEMP_DIR" 51 | 52 | echo "" 53 | echo "Done! Update the checksums in:" 54 | echo " - packaging/homebrew/floo.rb" 55 | echo " - packaging/aur/PKGBUILD" 56 | -------------------------------------------------------------------------------- /packaging/homebrew/floo.rb: -------------------------------------------------------------------------------- 1 | class Floo < Formula 2 | desc "Secure, high-performance tunneling in Zig. Expose your home services or access remote ones" 3 | homepage "https://github.com/YUX/floo" 4 | version "0.1.2" 5 | license "MIT" 6 | 7 | if Hardware::CPU.arm? 8 | url "https://github.com/YUX/floo/releases/download/v0.1.2/floo-aarch64-macos-m1.tar.gz" 9 | sha256 "ade1612e80eb7ea3ea2327d1fa791ae56435c2fbea0ce0aaa8fbb2b0adca6ab6" 10 | else 11 | url "https://github.com/YUX/floo/releases/download/v0.1.2/floo-x86_64-macos-haswell.tar.gz" 12 | sha256 "91853bd55977976f7934b20865cd1f3459a644abe966f208f08421f6300af29d" 13 | end 14 | 15 | def install 16 | bin.install "flooc" 17 | bin.install "floos" 18 | doc.install "README.md" 19 | (pkgshare/"examples").install "flooc.toml.example" 20 | (pkgshare/"examples").install "floos.toml.example" 21 | end 22 | 23 | def caveats 24 | <<~EOS 25 | Example configuration files are installed to: 26 | #{pkgshare}/examples/ 27 | 28 | To get started: 29 | 1. Copy example configs: cp #{pkgshare}/examples/*.toml.example . 30 | 2. Edit configs with your settings 31 | 3. Run: flooc flooc.toml (client) or floos floos.toml (server) 32 | 33 | See https://github.com/YUX/floo for complete documentation. 34 | EOS 35 | end 36 | 37 | test do 38 | system "#{bin}/flooc", "--version" 39 | system "#{bin}/floos", "--version" 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /.github/workflows/deploy-website.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Website 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - 'website/**' 8 | - '.github/workflows/deploy-website.yml' 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: read 13 | pages: write 14 | id-token: write 15 | 16 | concurrency: 17 | group: "pages" 18 | cancel-in-progress: false 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: '20' 31 | cache: 'npm' 32 | cache-dependency-path: website/package-lock.json 33 | 34 | - name: Install dependencies 35 | working-directory: ./website 36 | run: npm ci 37 | 38 | - name: Build website 39 | working-directory: ./website 40 | run: npm run build 41 | 42 | - name: Setup Pages 43 | uses: actions/configure-pages@v4 44 | 45 | - name: Upload artifact 46 | uses: actions/upload-pages-artifact@v3 47 | with: 48 | path: ./website/dist 49 | 50 | deploy: 51 | environment: 52 | name: github-pages 53 | url: ${{ steps.deployment.outputs.page_url }} 54 | runs-on: ubuntu-latest 55 | needs: build 56 | steps: 57 | - name: Deploy to GitHub Pages 58 | id: deployment 59 | uses: actions/deploy-pages@v4 60 | -------------------------------------------------------------------------------- /examples/expose-multiple-services/README.md: -------------------------------------------------------------------------------- 1 | # Expose Multiple Services with One flooc 2 | 3 | Serve Emby (HTTP) and SSH simultaneously through a single Floo client. The 4 | server publishes two public ports; flooc routes each connection back to the 5 | correct local target. 6 | 7 | ## Config recap 8 | 9 | - `floos.toml` defines two `[reverse_services]`: `emby` on `0.0.0.0:8096` and 10 | `ssh` on `0.0.0.0:2222`. Each has its own token so you can share media without 11 | exposing SSH credentials. 12 | - `flooc.toml` registers the same service names but points them to local 13 | addresses (`127.0.0.1:8096` and `127.0.0.1:22`). The SSH service overrides its 14 | token to match the server-side requirement. 15 | 16 | ## Launch sequence 17 | 18 | ```bash 19 | # On the VPS 20 | ./floos floos.toml 21 | 22 | # On the home lab machine 23 | ./flooc flooc.toml 24 | ``` 25 | 26 | Once the tunnel is up: 27 | 28 | - Friends visit `http://YOUR_SERVER_IP:8096` for Emby. 29 | - You connect via `ssh -p 2222 user@YOUR_SERVER_IP` for administration. 30 | 31 | ## Tips 32 | 33 | - Leave `num_tunnels = 0` to let Floo match your CPU cores automatically; set an 34 | explicit value (e.g. `num_tunnels = 4`) only when you want to cap or boost 35 | tunnel fan-out for specific workloads. 36 | - Add more sections under `[reverse_services]` to expose additional ports using 37 | the same tunnel. 38 | - Use per-service tokens to gate sensitive ports (SSH, admin dashboards) while 39 | keeping others broadly accessible. 40 | -------------------------------------------------------------------------------- /website/src/components/Features.css: -------------------------------------------------------------------------------- 1 | .features { 2 | background: var(--bg-secondary); 3 | position: relative; 4 | } 5 | 6 | .features::before { 7 | content: ''; 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | right: 0; 12 | height: 1px; 13 | background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent); 14 | } 15 | 16 | .features-grid { 17 | display: grid; 18 | grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); 19 | gap: 2rem; 20 | } 21 | 22 | .feature-card { 23 | background: var(--bg-tertiary); 24 | border: 1px solid rgba(0, 243, 255, 0.1); 25 | border-radius: 12px; 26 | padding: 2rem; 27 | transition: all 0.3s ease; 28 | animation: fade-in-up 0.6s ease-out both; 29 | } 30 | 31 | @keyframes fade-in-up { 32 | from { 33 | opacity: 0; 34 | transform: translateY(30px); 35 | } 36 | to { 37 | opacity: 1; 38 | transform: translateY(0); 39 | } 40 | } 41 | 42 | .feature-card:hover { 43 | border-color: var(--accent-cyan); 44 | box-shadow: var(--glow-cyan); 45 | transform: translateY(-5px); 46 | } 47 | 48 | .feature-icon { 49 | font-size: 3rem; 50 | margin-bottom: 1rem; 51 | filter: drop-shadow(0 0 10px rgba(0, 243, 255, 0.5)); 52 | } 53 | 54 | .feature-title { 55 | font-size: 1.5rem; 56 | font-weight: 700; 57 | margin-bottom: 1rem; 58 | color: var(--text-primary); 59 | } 60 | 61 | .feature-description { 62 | font-size: 1rem; 63 | color: var(--text-secondary); 64 | line-height: 1.6; 65 | } 66 | 67 | @media (max-width: 768px) { 68 | .features-grid { 69 | grid-template-columns: 1fr; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packaging/aur/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Your Name 2 | pkgname=floo 3 | pkgver=0.1.2 4 | pkgrel=1 5 | pkgdesc="Secure, high-performance tunneling in Zig. Expose your home services or access remote ones" 6 | arch=('x86_64' 'aarch64') 7 | url="https://github.com/YUX/floo" 8 | license=('MIT') 9 | provides=('floo' 'flooc' 'floos') 10 | conflicts=('floo-git') 11 | source_x86_64=("${pkgname}-${pkgver}-x86_64.tar.gz::https://github.com/YUX/floo/releases/download/v${pkgver}/floo-x86_64-linux-gnu-haswell.tar.gz") 12 | source_aarch64=("${pkgname}-${pkgver}-aarch64.tar.gz::https://github.com/YUX/floo/releases/download/v${pkgver}/floo-aarch64-linux-gnu.tar.gz") 13 | sha256sums_x86_64=('4087230211f3064c3644eb8cad115c94bfa34bc682c07191ea11228f3cd09a66') 14 | sha256sums_aarch64=('5f0c75550c0bb18fa72d547bc044d28f0eeefedbe399c1903ac06975c679a031') 15 | 16 | package() { 17 | cd "${srcdir}/${pkgname}-${pkgver}-${CARCH}" 18 | 19 | # Install binaries 20 | install -Dm755 flooc "${pkgdir}/usr/bin/flooc" 21 | install -Dm755 floos "${pkgdir}/usr/bin/floos" 22 | 23 | # Install documentation 24 | install -Dm644 README.md "${pkgdir}/usr/share/doc/${pkgname}/README.md" 25 | 26 | # Install example configs 27 | install -Dm644 flooc.toml.example "${pkgdir}/usr/share/doc/${pkgname}/examples/flooc.toml.example" 28 | install -Dm644 floos.toml.example "${pkgdir}/usr/share/doc/${pkgname}/examples/floos.toml.example" 29 | 30 | # Install license (if included in tarball) 31 | if [ -f LICENSE ]; then 32 | install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" 33 | fi 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | name: Test on ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, macos-latest] 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: Setup Zig 22 | uses: goto-bus-stop/setup-zig@v2 23 | with: 24 | version: master 25 | 26 | - name: Run tests 27 | run: zig build test 28 | 29 | - name: Build (Debug) 30 | run: zig build 31 | 32 | - name: Build (ReleaseFast) 33 | run: zig build -Doptimize=ReleaseFast 34 | 35 | - name: Test --help 36 | run: | 37 | ./zig-out/bin/floos --help 38 | ./zig-out/bin/flooc --help 39 | 40 | - name: Test --version 41 | run: | 42 | ./zig-out/bin/floos --version 43 | ./zig-out/bin/flooc --version 44 | 45 | - name: Test --doctor (should show warnings without config) 46 | run: | 47 | ./zig-out/bin/floos --doctor || true 48 | ./zig-out/bin/flooc --doctor || true 49 | 50 | format-check: 51 | name: Check Formatting 52 | runs-on: ubuntu-latest 53 | 54 | steps: 55 | - name: Checkout code 56 | uses: actions/checkout@v4 57 | 58 | - name: Setup Zig 59 | uses: goto-bus-stop/setup-zig@v2 60 | with: 61 | version: master 62 | 63 | - name: Check formatting 64 | run: | 65 | zig fmt --check src/*.zig build.zig 66 | -------------------------------------------------------------------------------- /configs/README.md: -------------------------------------------------------------------------------- 1 | # Configuration Templates 2 | 3 | The `configs/` directory contains ready-to-use TOML templates for both the Floo 4 | server (`floos`) and client (`flooc`). Each template follows the exact format 5 | implemented in `src/config.zig`: 6 | 7 | 1. **Core tunnel settings** – listener/bind information plus cipher, PSK, token 8 | 2. **Services** – what to forward (server-side) or expose/listen for (client-side) 9 | 3. **Advanced tuning** – optional knobs for TCP, UDP, heartbeats, proxies, etc. 10 | 11 | ## Available templates 12 | 13 | | File | Description | 14 | |------|-------------| 15 | | `floos.example.toml` | Comprehensive server config with comments on every option | 16 | | `flooc.example.toml` | Comprehensive client config covering forward + reverse setups | 17 | | `floos.minimal.toml` | Minimal server config for quick smoke tests | 18 | | `flooc.minimal.toml` | Minimal client config for exposing one local service | 19 | 20 | ## Using a template 21 | 22 | ```bash 23 | cp configs/floos.example.toml floos.toml 24 | cp configs/flooc.example.toml flooc.toml 25 | ``` 26 | 27 | Update **at least** the `psk`, `token`, and any hostnames/IPs. The server must be 28 | started before the client: 29 | 30 | ```bash 31 | ./floos floos.toml # on your VPS/cloud host 32 | ./flooc flooc.toml # on your laptop/home server 33 | ``` 34 | 35 | ## Per-service overrides 36 | 37 | Inside the `[services]` or `[reverse_services]` sections you can override 38 | service-specific settings using dotted keys: 39 | 40 | ```toml 41 | [services] 42 | ssh = "127.0.0.1:2222" 43 | ssh.token = "ssh-only-token" 44 | ``` 45 | 46 | Tokens fall back to the global `token` when no override is provided. Reverse 47 | services behave the same way. 48 | 49 | ## More examples 50 | 51 | For end-to-end walkthroughs (home media, database access, proxies, etc.), check 52 | [`examples/`](../examples/). 53 | -------------------------------------------------------------------------------- /website/src/components/Footer.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | background: var(--bg-secondary); 3 | border-top: 1px solid rgba(0, 243, 255, 0.2); 4 | padding: 4rem 0 2rem; 5 | } 6 | 7 | .footer-content { 8 | display: grid; 9 | grid-template-columns: 2fr 3fr; 10 | gap: 4rem; 11 | margin-bottom: 3rem; 12 | } 13 | 14 | .footer-brand { 15 | max-width: 300px; 16 | } 17 | 18 | .footer-logo { 19 | margin-bottom: 1rem; 20 | } 21 | 22 | .logo-text { 23 | font-size: 2rem; 24 | font-weight: 800; 25 | background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple)); 26 | -webkit-background-clip: text; 27 | -webkit-text-fill-color: transparent; 28 | background-clip: text; 29 | } 30 | 31 | .footer-tagline { 32 | color: var(--text-secondary); 33 | line-height: 1.6; 34 | } 35 | 36 | .footer-links { 37 | display: grid; 38 | grid-template-columns: repeat(3, 1fr); 39 | gap: 2rem; 40 | } 41 | 42 | .footer-column { 43 | display: flex; 44 | flex-direction: column; 45 | gap: 0.75rem; 46 | } 47 | 48 | .footer-title { 49 | font-size: 1rem; 50 | font-weight: 700; 51 | color: var(--text-primary); 52 | margin-bottom: 0.5rem; 53 | } 54 | 55 | .footer-column a { 56 | color: var(--text-secondary); 57 | text-decoration: none; 58 | font-size: 0.9375rem; 59 | transition: color 0.3s ease; 60 | } 61 | 62 | .footer-column a:hover { 63 | color: var(--accent-cyan); 64 | } 65 | 66 | .footer-bottom { 67 | display: flex; 68 | justify-content: space-between; 69 | align-items: center; 70 | padding-top: 2rem; 71 | border-top: 1px solid rgba(0, 243, 255, 0.1); 72 | color: var(--text-tertiary); 73 | font-size: 0.875rem; 74 | } 75 | 76 | .footer-built { 77 | display: flex; 78 | align-items: center; 79 | gap: 0.5rem; 80 | } 81 | 82 | @media (max-width: 768px) { 83 | .footer-content { 84 | grid-template-columns: 1fr; 85 | gap: 2rem; 86 | } 87 | 88 | .footer-links { 89 | grid-template-columns: 1fr; 90 | } 91 | 92 | .footer-bottom { 93 | flex-direction: column; 94 | gap: 1rem; 95 | text-align: center; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /examples/reverse-forwarding-emby/README.md: -------------------------------------------------------------------------------- 1 | # Reverse Forwarding: Emby/Jellyfin 2 | 3 | Expose a home media server by running floos on a VPS and flooc on the machine 4 | that hosts Emby/Jellyfin. 5 | 6 | ## Flow 7 | 8 | ``` 9 | Home LAN Public VPS 10 | ┌─────────────┐ ┌───────────────────────┐ 11 | │ Emby :8096 │ ← reverse tunnel ←── │ floos :8443 + :8096 │ 12 | │ flooc │ ─── encrypted ───▶ │ publishes http port │ 13 | └─────────────┘ └───────────────────────┘ 14 | ``` 15 | 16 | ## Configuration summary 17 | 18 | 1. **Server (`floos.toml`)** – bind to `0.0.0.0:8443`, create `[reverse_services]` 19 | entry such as `emby = "0.0.0.0:8096"`. 20 | 2. **Client (`flooc.toml`)** – point `server = "VPS_IP:8443"` and mirror the 21 | service locally with `emby = "127.0.0.1:8096"`. 22 | 3. Use the **same** PSK + token on both sides. 23 | 24 | Files in this folder are ready to copy, just replace the secrets and IPs. 25 | 26 | ## Start the tunnel 27 | 28 | ```bash 29 | # On the VPS or Raspberry Pi 30 | ./floos floos.toml 31 | 32 | # On the home server 33 | ./flooc flooc.toml 34 | ``` 35 | 36 | Optional: run `./flooc --doctor flooc.toml` to verify reachability without 37 | starting the reverse listener. 38 | 39 | ## Hardening + tuning 40 | 41 | - Leave `num_tunnels = 0` to auto-scale with CPU cores; bump it only if you need 42 | more dedicated tunnels than the hardware provides. 43 | - Bump `socket_buffer_size` beyond 512 KB when pushing 4K video across 44 | high-latency links. 45 | - Combine Floo with an HTTPS reverse proxy (Caddy/Traefik/nginx) on the VPS if 46 | you need browser TLS certificates. 47 | 48 | ## Troubleshooting checklist 49 | 50 | | Issue | Checks | 51 | |-------|--------| 52 | | Handshake fails | Cipher/PSK/token mismatch, firewall blocking 8443 | 53 | | Reverse port closed | Confirm floos is running and listening on the publish port | 54 | | Streaming stutters | Inspect `socket_buffer_size`, internet uplink, or enable multiple tunnels | 55 | | flooc disconnects | Look at `reconnect_*` settings and ISP router logs | 56 | -------------------------------------------------------------------------------- /website/src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | :root { 8 | /* Dark cyberpunk color palette */ 9 | --bg-primary: #0a0a0f; 10 | --bg-secondary: #13131a; 11 | --bg-tertiary: #1a1a24; 12 | 13 | --accent-cyan: #00f3ff; 14 | --accent-pink: #ff006e; 15 | --accent-purple: #8b5cf6; 16 | --accent-yellow: #ffd60a; 17 | 18 | --text-primary: #ffffff; 19 | --text-secondary: #a0a0b0; 20 | --text-tertiary: #6b6b7b; 21 | 22 | --glow-cyan: 0 0 20px rgba(0, 243, 255, 0.5); 23 | --glow-pink: 0 0 20px rgba(255, 0, 110, 0.5); 24 | --glow-purple: 0 0 20px rgba(139, 92, 246, 0.5); 25 | } 26 | 27 | body { 28 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 29 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 30 | sans-serif; 31 | -webkit-font-smoothing: antialiased; 32 | -moz-osx-font-smoothing: grayscale; 33 | background-color: var(--bg-primary); 34 | color: var(--text-primary); 35 | overflow-x: hidden; 36 | } 37 | 38 | code { 39 | font-family: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace; 40 | background: var(--bg-tertiary); 41 | padding: 0.2em 0.4em; 42 | border-radius: 3px; 43 | font-size: 0.9em; 44 | border: 1px solid rgba(0, 243, 255, 0.2); 45 | } 46 | 47 | pre { 48 | background: var(--bg-tertiary); 49 | border: 1px solid rgba(0, 243, 255, 0.2); 50 | border-radius: 8px; 51 | padding: 1.5rem; 52 | overflow-x: auto; 53 | margin: 1rem 0; 54 | box-shadow: var(--glow-cyan); 55 | } 56 | 57 | pre code { 58 | background: none; 59 | padding: 0; 60 | border: none; 61 | font-size: 0.9rem; 62 | line-height: 1.6; 63 | } 64 | 65 | ::selection { 66 | background: var(--accent-cyan); 67 | color: var(--bg-primary); 68 | } 69 | 70 | /* Scrollbar styling */ 71 | ::-webkit-scrollbar { 72 | width: 10px; 73 | } 74 | 75 | ::-webkit-scrollbar-track { 76 | background: var(--bg-secondary); 77 | } 78 | 79 | ::-webkit-scrollbar-thumb { 80 | background: var(--accent-cyan); 81 | border-radius: 5px; 82 | } 83 | 84 | ::-webkit-scrollbar-thumb:hover { 85 | background: var(--accent-purple); 86 | } 87 | -------------------------------------------------------------------------------- /src/diagnostics.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const CheckStatus = enum { ok, warn, fail }; 4 | 5 | pub fn reportCheck(status: CheckStatus, comptime fmt: []const u8, args: anytype) void { 6 | const prefix = switch (status) { 7 | .ok => "[OK] ", 8 | .warn => "[WARN] ", 9 | .fail => "[FAIL] ", 10 | }; 11 | std.debug.print("{s}", .{prefix}); 12 | std.debug.print(fmt, args); 13 | std.debug.print("\n", .{}); 14 | } 15 | 16 | pub fn flushEncryptStats(prefix: []const u8, total: *std.atomic.Value(u64), calls: *std.atomic.Value(u64)) void { 17 | const total_ns = total.load(.acquire); 18 | const call_count = calls.load(.acquire); 19 | if (call_count == 0 or total_ns == 0) return; 20 | 21 | const avg = total_ns / call_count; 22 | std.debug.print("[PROFILE] {s} encryption total={} ns calls={} avg={} ns\n", .{ prefix, total_ns, call_count, avg }); 23 | appendProfileLine(prefix, total_ns, call_count, avg); 24 | } 25 | 26 | pub fn flushThroughputStats( 27 | prefix: []const u8, 28 | tx: *std.atomic.Value(u64), 29 | rx: *std.atomic.Value(u64), 30 | ) void { 31 | const tx_bytes = tx.load(.acquire); 32 | const rx_bytes = rx.load(.acquire); 33 | if (tx_bytes == 0 and rx_bytes == 0) return; 34 | 35 | const tx_mb = asDecimalMB(tx_bytes); 36 | const rx_mb = asDecimalMB(rx_bytes); 37 | std.debug.print( 38 | "[PROFILE] {s} throughput tx={} bytes ({d:.2} MB) rx={} bytes ({d:.2} MB)\n", 39 | .{ prefix, tx_bytes, tx_mb, rx_bytes, rx_mb }, 40 | ); 41 | } 42 | 43 | fn asDecimalMB(bytes: u64) f64 { 44 | return @as(f64, @floatFromInt(bytes)) / (1024.0 * 1024.0); 45 | } 46 | 47 | fn appendProfileLine(prefix: []const u8, total: u64, calls: u64, avg: u64) void { 48 | const path = "/tmp/floo_profile.log"; 49 | _ = std.fs.createFileAbsolute(path, .{ .truncate = false, .read = false }) catch {}; 50 | 51 | var file = std.fs.openFileAbsolute(path, .{ .mode = .write_only }) catch return; 52 | defer file.close(); 53 | 54 | _ = file.seekFromEnd(0) catch {}; 55 | 56 | var buf: [128]u8 = undefined; 57 | const line = std.fmt.bufPrint(&buf, "{s}\ttotal_ns={}\tcalls={}\tavg_ns={}\n", .{ prefix, total, calls, avg }) catch return; 58 | file.writeAll(line) catch {}; 59 | } 60 | -------------------------------------------------------------------------------- /website/src/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | min-height: 100vh; 3 | background: var(--bg-primary); 4 | position: relative; 5 | } 6 | 7 | /* Background grid effect */ 8 | .app::before { 9 | content: ''; 10 | position: fixed; 11 | top: 0; 12 | left: 0; 13 | width: 100%; 14 | height: 100%; 15 | background-image: 16 | linear-gradient(rgba(0, 243, 255, 0.03) 1px, transparent 1px), 17 | linear-gradient(90deg, rgba(0, 243, 255, 0.03) 1px, transparent 1px); 18 | background-size: 50px 50px; 19 | pointer-events: none; 20 | z-index: 0; 21 | } 22 | 23 | .app > * { 24 | position: relative; 25 | z-index: 1; 26 | } 27 | 28 | .container { 29 | max-width: 1200px; 30 | margin: 0 auto; 31 | padding: 0 2rem; 32 | } 33 | 34 | .section { 35 | padding: 6rem 0; 36 | } 37 | 38 | .section-title { 39 | font-size: 3rem; 40 | font-weight: 700; 41 | text-align: center; 42 | margin-bottom: 3rem; 43 | background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple)); 44 | -webkit-background-clip: text; 45 | -webkit-text-fill-color: transparent; 46 | background-clip: text; 47 | } 48 | 49 | .btn { 50 | display: inline-flex; 51 | align-items: center; 52 | gap: 0.5rem; 53 | padding: 0.875rem 2rem; 54 | font-size: 1.1rem; 55 | font-weight: 600; 56 | border: none; 57 | border-radius: 8px; 58 | cursor: pointer; 59 | transition: all 0.3s ease; 60 | text-decoration: none; 61 | position: relative; 62 | overflow: hidden; 63 | } 64 | 65 | .btn-primary { 66 | background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple)); 67 | color: var(--bg-primary); 68 | box-shadow: var(--glow-cyan); 69 | } 70 | 71 | .btn-primary:hover { 72 | transform: translateY(-2px); 73 | box-shadow: var(--glow-purple); 74 | } 75 | 76 | .btn-secondary { 77 | background: var(--bg-tertiary); 78 | color: var(--text-primary); 79 | border: 2px solid var(--accent-cyan); 80 | } 81 | 82 | .btn-secondary:hover { 83 | background: var(--accent-cyan); 84 | color: var(--bg-primary); 85 | box-shadow: var(--glow-cyan); 86 | transform: translateY(-2px); 87 | } 88 | 89 | @media (max-width: 768px) { 90 | .section { 91 | padding: 3rem 0; 92 | } 93 | 94 | .section-title { 95 | font-size: 2rem; 96 | } 97 | 98 | .container { 99 | padding: 0 1rem; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /website/src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import './Footer.css' 2 | 3 | export default function Footer() { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 | Floo 11 |
12 |

13 | High-throughput tunneling in Zig 14 |

15 |
16 | 17 |
18 |
19 |

Project

20 | GitHub 21 | Releases 22 | Issues 23 |
24 | 25 |
26 |

Documentation

27 | README 28 | Configuration 29 | Troubleshooting 30 |
31 | 32 |
33 |

Community

34 | Discussions 35 | Contributing 36 | License 37 |
38 |
39 |
40 | 41 |
42 |

© {new Date().getFullYear()} Floo. Licensed under MIT License.

43 |

Built with ❤️ in Zig

44 |
45 |
46 |
47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /website/src/components/Features.jsx: -------------------------------------------------------------------------------- 1 | import './Features.css' 2 | 3 | export default function Features() { 4 | const features = [ 5 | { 6 | icon: '⭐', 7 | title: 'Zero Dependencies', 8 | description: 'Built with only Zig stdlib. No external dependencies, supply chain, or security vulnerabilities.' 9 | }, 10 | { 11 | icon: '🔐', 12 | title: 'Noise Protocol', 13 | description: 'Secure authentication with Noise XX + PSK. Choose from 5 AEAD ciphers including AEGIS-128L and AES-256-GCM.' 14 | }, 15 | { 16 | icon: '🚀', 17 | title: 'Multiplexing', 18 | description: 'Expose multiple services through a single tunnel connection with intelligent routing.' 19 | }, 20 | { 21 | icon: '⚡', 22 | title: 'Parallel Tunnels', 23 | description: 'Automatically matches your CPU cores and pins tunnel threads for maximum throughput.' 24 | }, 25 | { 26 | icon: '📈', 27 | title: 'Live Metrics', 28 | description: 'Emit per-tunnel throughput and crypto timing with a single SIGUSR1—no extra agent required.' 29 | }, 30 | { 31 | icon: '🌐', 32 | title: 'Proxy Support', 33 | description: 'Connect through SOCKS5 or HTTP CONNECT proxies for network flexibility.' 34 | }, 35 | { 36 | icon: '💓', 37 | title: 'Auto-Reconnect', 38 | description: 'Heartbeat supervision with automatic reconnection. Never lose your tunnel.' 39 | }, 40 | { 41 | icon: '📝', 42 | title: 'Config Management', 43 | description: 'Tweak TOML configs and restart floos/flooc to apply changes while we rework live reload.' 44 | }, 45 | { 46 | icon: '📊', 47 | title: 'Built-in Diagnostics', 48 | description: 'Debug with --doctor and --ping commands. Troubleshooting made easy.' 49 | }, 50 | { 51 | icon: '🎯', 52 | title: 'Token Auth', 53 | description: 'Per-service token authentication for fine-grained access control.' 54 | } 55 | ] 56 | 57 | return ( 58 |
59 |
60 |

Features

61 | 62 |
63 | {features.map((feature, index) => ( 64 |
65 |
{feature.icon}
66 |

{feature.title}

67 |

{feature.description}

68 |
69 | ))} 70 |
71 |
72 |
73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Floo Website 2 | 3 | Modern, cyberpunk-themed project website for Floo, built with React and Vite. 4 | 5 | ## Features 6 | 7 | - 🎨 Dark/cyberpunk themed design with neon accents 8 | - ⚡ Animated hero section with performance metrics 9 | - 📊 Interactive performance benchmarks 10 | - 🔗 Live GitHub integration (stars, releases) 11 | - 📱 Fully responsive design 12 | - 🚀 Fast and optimized with Vite 13 | 14 | ## Development 15 | 16 | ### Prerequisites 17 | 18 | - Node.js 18+ and npm 19 | 20 | ### Install Dependencies 21 | 22 | ```bash 23 | npm install 24 | ``` 25 | 26 | ### Run Development Server 27 | 28 | ```bash 29 | npm run dev 30 | ``` 31 | 32 | The website will be available at `http://localhost:5173` 33 | 34 | ### Build for Production 35 | 36 | ```bash 37 | npm run build 38 | ``` 39 | 40 | The built files will be in the `dist/` directory. 41 | 42 | ### Preview Production Build 43 | 44 | ```bash 45 | npm run preview 46 | ``` 47 | 48 | ## Deployment 49 | 50 | The website is automatically deployed to GitHub Pages when changes are pushed to the `main` branch (in the `website/` directory). 51 | 52 | The deployment is handled by `.github/workflows/deploy-website.yml`. 53 | 54 | ### Manual Deployment 55 | 56 | If you need to deploy manually: 57 | 58 | 1. Build the site: `npm run build` 59 | 2. The built files in `dist/` can be deployed to any static hosting service 60 | 61 | ## Project Structure 62 | 63 | ``` 64 | website/ 65 | ├── src/ 66 | │ ├── components/ # React components 67 | │ │ ├── Hero.jsx # Hero section with animated metrics 68 | │ │ ├── Features.jsx # Features grid 69 | │ │ ├── Performance.jsx # Performance benchmarks 70 | │ │ ├── Installation.jsx # Installation guide 71 | │ │ ├── GitHub.jsx # GitHub stats integration 72 | │ │ └── Footer.jsx # Footer 73 | │ ├── App.jsx # Main app component 74 | │ ├── App.css # Global app styles 75 | │ ├── index.css # Global CSS variables and theme 76 | │ └── main.jsx # Entry point 77 | ├── index.html # HTML template 78 | ├── vite.config.js # Vite configuration 79 | └── package.json # Dependencies and scripts 80 | ``` 81 | 82 | ## Customization 83 | 84 | ### Colors 85 | 86 | Edit the CSS variables in `src/index.css`: 87 | 88 | ```css 89 | :root { 90 | --accent-cyan: #00f3ff; 91 | --accent-pink: #ff006e; 92 | --accent-purple: #8b5cf6; 93 | /* ... */ 94 | } 95 | ``` 96 | 97 | ### Content 98 | 99 | - **Performance metrics**: Edit in `src/components/Performance.jsx` 100 | - **Features**: Edit the `features` array in `src/components/Features.jsx` 101 | - **Platform downloads**: Edit the `platforms` array in `src/components/Installation.jsx` 102 | 103 | ## License 104 | 105 | MIT License - Same as the main Floo project 106 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to Floo will be documented in this file. 4 | 5 | ## [0.1.5] - 2025-11-19 6 | 7 | ### Changed 8 | - Migrated to Zig master branch (0.16.0-dev) for latest performance improvements. 9 | - Updated CI/CD workflows to support Zig master. 10 | - Fixed Linux compilation issues regarding `sigset_t` initialization. 11 | 12 | ### Performance 13 | - **22.2 Gbps** AES-128-GCM throughput (Reverse Mode, M1). 14 | - AES-GCM now outperforms AEGIS ciphers on hardware with AES-NI/ARMv8 Crypto extensions. 15 | - Outperforms Rathole by ~1.5x and FRP by ~2.1x. 16 | 17 | ## [0.1.4] - 2025-11-09 18 | 19 | ### Performance 20 | - **30.9 Gbps** plaintext throughput (single stream, M1) 21 | - **22.1 Gbps** AEGIS-128L encrypted throughput (single stream, M1) 22 | - **23.3 Gbps** AEGIS-128L reverse mode (single stream, M1) 23 | - 3.4x faster than FRP in single-stream benchmarks 24 | - 1.3x faster than Rathole with AEGIS-128L 25 | - Multi-stream performance: 7.7-9.6 Gbps (4 concurrent streams) 26 | 27 | ### Changed 28 | - Updated benchmark methodology for better accuracy (single-stream testing) 29 | - Enhanced performance testing coverage across all cipher types 30 | - Improved documentation with comprehensive architecture explanation 31 | 32 | ### Documentation 33 | - Added detailed architecture deep-dive 34 | - Updated benchmark results with single-stream and multi-stream comparisons 35 | - Added cipher performance comparison tables 36 | - Documented data flow paths and design patterns 37 | 38 | ## [0.1.3] - 2024-11-08 39 | 40 | ### Added 41 | - Reference counting for streams and connections to prevent use-after-free bugs 42 | - Comprehensive reverse forwarding examples (Emby/Jellyfin) 43 | - Multi-client load balancing example 44 | - Corporate proxy tunneling example 45 | - Dedicated `configs/` directory for configuration templates 46 | - Socket buffer size configuration support (up to 8MB) 47 | 48 | ### Changed 49 | - Simplified TOML configuration format 50 | - Improved stream lifecycle management with proper cleanup 51 | - Updated benchmark script to test both forward and reverse modes 52 | - Reorganized project structure for better clarity 53 | - Enhanced documentation with clearer examples 54 | 55 | ### Fixed 56 | - **Critical**: Reverse forwarding crashes after first request 57 | - **Critical**: Use-after-free vulnerability in ReverseListener 58 | - **Critical**: Mutex deadlocks during blocking I/O operations 59 | - Memory leaks in stream and connection cleanup 60 | - Signal handling for SIGPIPE and EINTR 61 | - Hardware crypto acceleration on ARM processors (restored 22+ Gbps performance) 62 | 63 | ### Performance 64 | - AEGIS-128L: 22.6 Gbps (encrypted with hardware acceleration) 65 | - AES-256-GCM: 18.0 Gbps (2.2x faster than FRP) 66 | - Plaintext: 28+ Gbps single stream 67 | - Reverse mode now performs as well as forward mode 68 | 69 | ### Security 70 | - Fixed timing attack vulnerability in token comparison 71 | - All PSK comparisons now use constant-time equality checks 72 | 73 | ## [0.1.2] - Previous Release 74 | 75 | Initial stable release with basic forward and reverse tunneling support. -------------------------------------------------------------------------------- /website/src/components/GitHub.css: -------------------------------------------------------------------------------- 1 | .github { 2 | background: var(--bg-primary); 3 | } 4 | 5 | .github-grid { 6 | display: grid; 7 | grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); 8 | gap: 2rem; 9 | margin-bottom: 3rem; 10 | } 11 | 12 | .github-card { 13 | background: var(--bg-tertiary); 14 | border: 1px solid rgba(0, 243, 255, 0.2); 15 | border-radius: 16px; 16 | padding: 2.5rem; 17 | transition: all 0.3s ease; 18 | } 19 | 20 | .github-card:hover { 21 | border-color: var(--accent-cyan); 22 | box-shadow: var(--glow-cyan); 23 | transform: translateY(-5px); 24 | } 25 | 26 | .card-icon { 27 | font-size: 3rem; 28 | margin-bottom: 1.5rem; 29 | filter: drop-shadow(0 0 10px rgba(0, 243, 255, 0.5)); 30 | } 31 | 32 | .card-title { 33 | font-size: 1.75rem; 34 | font-weight: 700; 35 | margin-bottom: 1.5rem; 36 | color: var(--text-primary); 37 | } 38 | 39 | .stats-grid { 40 | display: grid; 41 | grid-template-columns: repeat(3, 1fr); 42 | gap: 1.5rem; 43 | margin-bottom: 2rem; 44 | } 45 | 46 | .stat-item { 47 | text-align: center; 48 | } 49 | 50 | .stat-value { 51 | font-size: 2rem; 52 | font-weight: 800; 53 | background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple)); 54 | -webkit-background-clip: text; 55 | -webkit-text-fill-color: transparent; 56 | background-clip: text; 57 | margin-bottom: 0.5rem; 58 | } 59 | 60 | .stat-label { 61 | font-size: 0.875rem; 62 | color: var(--text-tertiary); 63 | text-transform: uppercase; 64 | letter-spacing: 0.05em; 65 | } 66 | 67 | .release-info { 68 | margin-bottom: 2rem; 69 | } 70 | 71 | .release-version { 72 | font-size: 2.5rem; 73 | font-weight: 800; 74 | color: var(--accent-cyan); 75 | font-family: 'Monaco', monospace; 76 | margin-bottom: 0.5rem; 77 | } 78 | 79 | .release-date { 80 | font-size: 1rem; 81 | color: var(--text-tertiary); 82 | } 83 | 84 | .card-text { 85 | font-size: 1rem; 86 | color: var(--text-secondary); 87 | line-height: 1.6; 88 | margin-bottom: 2rem; 89 | } 90 | 91 | .card-link { 92 | display: inline-flex; 93 | align-items: center; 94 | gap: 0.5rem; 95 | color: var(--accent-cyan); 96 | font-weight: 600; 97 | text-decoration: none; 98 | transition: all 0.3s ease; 99 | } 100 | 101 | .card-link:hover { 102 | color: var(--accent-purple); 103 | transform: translateX(5px); 104 | } 105 | 106 | .license-info { 107 | display: flex; 108 | align-items: center; 109 | justify-content: center; 110 | gap: 0.75rem; 111 | padding: 1.5rem; 112 | background: rgba(0, 243, 255, 0.05); 113 | border: 1px solid rgba(0, 243, 255, 0.2); 114 | border-radius: 8px; 115 | color: var(--text-secondary); 116 | font-size: 1rem; 117 | } 118 | 119 | .license-info svg { 120 | color: var(--accent-cyan); 121 | } 122 | 123 | @media (max-width: 768px) { 124 | .github-grid { 125 | grid-template-columns: 1fr; 126 | } 127 | 128 | .stats-grid { 129 | grid-template-columns: repeat(3, 1fr); 130 | gap: 1rem; 131 | } 132 | 133 | .stat-value { 134 | font-size: 1.5rem; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /packaging/snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: floo 2 | base: core22 3 | version: '0.1.2' 4 | summary: Secure, high-performance tunneling in Zig 5 | description: | 6 | Floo is a high-throughput tunneling solution written in Zig with zero 7 | dependencies. It provides secure tunneling using Noise XX protocol with 8 | multiple AEAD ciphers. 9 | 10 | Features: 11 | - 29.4 Gbps throughput with AEGIS-128L cipher 12 | - Zero runtime dependencies 13 | - Reverse and forward tunneling modes 14 | - SOCKS5 and HTTP CONNECT proxy support 15 | - Config changes applied on restart (SIGHUP reload temporarily disabled) 16 | - Built-in diagnostics (--doctor, --ping) 17 | - Automatic reconnection with exponential backoff 18 | 19 | This snap includes both the client (flooc) and server (floos) binaries. 20 | 21 | grade: stable 22 | confinement: strict 23 | 24 | architectures: 25 | - build-on: amd64 26 | - build-on: arm64 27 | 28 | apps: 29 | flooc: 30 | command: bin/flooc 31 | plugs: 32 | - network 33 | - network-bind 34 | - home 35 | 36 | floos: 37 | command: bin/floos 38 | plugs: 39 | - network 40 | - network-bind 41 | - network-control 42 | - home 43 | daemon: simple 44 | restart-condition: on-failure 45 | 46 | parts: 47 | floo: 48 | plugin: dump 49 | source: https://github.com/YUX/floo/releases/download/v${SNAPCRAFT_PROJECT_VERSION}/floo-${SNAPCRAFT_ARCH_TRIPLET}.tar.gz 50 | source-type: tar 51 | override-build: | 52 | # Determine the correct artifact name based on architecture 53 | case "${SNAPCRAFT_TARGET_ARCH}" in 54 | amd64) 55 | ARTIFACT="floo-x86_64-linux-gnu-haswell.tar.gz" 56 | ;; 57 | arm64) 58 | ARTIFACT="floo-aarch64-linux-gnu.tar.gz" 59 | ;; 60 | *) 61 | echo "Unsupported architecture: ${SNAPCRAFT_TARGET_ARCH}" 62 | exit 1 63 | ;; 64 | esac 65 | 66 | # Download the correct artifact 67 | wget -O /tmp/floo.tar.gz "https://github.com/YUX/floo/releases/download/v${SNAPCRAFT_PROJECT_VERSION}/${ARTIFACT}" 68 | 69 | # Extract 70 | mkdir -p /tmp/floo-extract 71 | tar xzf /tmp/floo.tar.gz -C /tmp/floo-extract --strip-components=1 72 | 73 | # Install binaries 74 | mkdir -p ${SNAPCRAFT_PART_INSTALL}/bin 75 | cp /tmp/floo-extract/flooc ${SNAPCRAFT_PART_INSTALL}/bin/ 76 | cp /tmp/floo-extract/floos ${SNAPCRAFT_PART_INSTALL}/bin/ 77 | chmod 755 ${SNAPCRAFT_PART_INSTALL}/bin/flooc 78 | chmod 755 ${SNAPCRAFT_PART_INSTALL}/bin/floos 79 | 80 | # Install documentation 81 | mkdir -p ${SNAPCRAFT_PART_INSTALL}/share/doc/floo 82 | cp /tmp/floo-extract/README.md ${SNAPCRAFT_PART_INSTALL}/share/doc/floo/ 83 | 84 | # Install example configs 85 | mkdir -p ${SNAPCRAFT_PART_INSTALL}/share/doc/floo/examples 86 | cp /tmp/floo-extract/*.toml.example ${SNAPCRAFT_PART_INSTALL}/share/doc/floo/examples/ 87 | 88 | # Cleanup 89 | rm -rf /tmp/floo.tar.gz /tmp/floo-extract 90 | build-packages: 91 | - wget 92 | stage: 93 | - bin/flooc 94 | - bin/floos 95 | - share/doc/floo/* 96 | 97 | plugs: 98 | home: 99 | read: all 100 | -------------------------------------------------------------------------------- /.github/workflows/publish-snap.yml: -------------------------------------------------------------------------------- 1 | name: Publish Snap 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: 'Version to publish (e.g., 0.1.2)' 10 | required: true 11 | 12 | jobs: 13 | build-snap: 14 | name: Build and Publish Snap 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | architecture: 20 | - amd64 21 | - arm64 22 | 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v4 26 | 27 | - name: Determine version 28 | id: version 29 | run: | 30 | if [ "${{ github.event_name }}" = "release" ]; then 31 | VERSION="${GITHUB_REF#refs/tags/v}" 32 | else 33 | VERSION="${{ github.event.inputs.version }}" 34 | fi 35 | echo "version=${VERSION}" >> $GITHUB_OUTPUT 36 | echo "Building version: ${VERSION}" 37 | 38 | - name: Update snapcraft.yaml version 39 | run: | 40 | cd packaging/snap 41 | sed -i "s/^version:.*/version: '${{ steps.version.outputs.version }}'/" snapcraft.yaml 42 | 43 | - name: Build Snap 44 | uses: snapcore/action-build@v1 45 | id: build-snap 46 | with: 47 | path: packaging/snap 48 | 49 | - name: Upload Snap artifact 50 | uses: actions/upload-artifact@v4 51 | with: 52 | name: snap-${{ matrix.architecture }} 53 | path: ${{ steps.build-snap.outputs.snap }} 54 | 55 | - name: Publish to Snap Store 56 | if: github.event_name == 'release' && secrets.SNAPCRAFT_TOKEN != '' 57 | uses: snapcore/action-publish@v1 58 | env: 59 | SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }} 60 | with: 61 | snap: ${{ steps.build-snap.outputs.snap }} 62 | release: stable 63 | 64 | publish-summary: 65 | name: Publish Summary 66 | runs-on: ubuntu-latest 67 | needs: build-snap 68 | if: always() 69 | 70 | steps: 71 | - name: Summary 72 | run: | 73 | echo "## Snap Build Complete" >> $GITHUB_STEP_SUMMARY 74 | echo "" >> $GITHUB_STEP_SUMMARY 75 | echo "Version: ${{ needs.build-snap.outputs.version || github.event.inputs.version }}" >> $GITHUB_STEP_SUMMARY 76 | echo "" >> $GITHUB_STEP_SUMMARY 77 | 78 | if [ "${{ secrets.SNAPCRAFT_TOKEN }}" != "" ]; then 79 | echo "✅ Published to Snap Store" >> $GITHUB_STEP_SUMMARY 80 | echo "" >> $GITHUB_STEP_SUMMARY 81 | echo "Users can install with:" >> $GITHUB_STEP_SUMMARY 82 | echo '```bash' >> $GITHUB_STEP_SUMMARY 83 | echo "sudo snap install floo" >> $GITHUB_STEP_SUMMARY 84 | echo '```' >> $GITHUB_STEP_SUMMARY 85 | else 86 | echo "⚠️ Not published (SNAPCRAFT_TOKEN secret not set)" >> $GITHUB_STEP_SUMMARY 87 | echo "" >> $GITHUB_STEP_SUMMARY 88 | echo "To enable automatic publishing:" >> $GITHUB_STEP_SUMMARY 89 | echo "1. Register on https://snapcraft.io/" >> $GITHUB_STEP_SUMMARY 90 | echo "2. Export credentials: snapcraft export-login --snaps=floo --channels=stable -" >> $GITHUB_STEP_SUMMARY 91 | echo "3. Add as SNAPCRAFT_TOKEN secret in GitHub repo" >> $GITHUB_STEP_SUMMARY 92 | fi 93 | -------------------------------------------------------------------------------- /website/src/components/Performance.css: -------------------------------------------------------------------------------- 1 | .performance { 2 | background: var(--bg-primary); 3 | } 4 | 5 | .performance-subtitle { 6 | text-align: center; 7 | color: var(--text-tertiary); 8 | font-size: 1rem; 9 | margin-bottom: 3rem; 10 | font-family: 'Monaco', monospace; 11 | } 12 | 13 | .benchmark-chart { 14 | max-width: 900px; 15 | margin: 0 auto 4rem; 16 | background: var(--bg-tertiary); 17 | border: 1px solid rgba(0, 243, 255, 0.2); 18 | border-radius: 16px; 19 | padding: 3rem; 20 | } 21 | 22 | .benchmark-row { 23 | margin-bottom: 2.5rem; 24 | } 25 | 26 | .benchmark-row:last-child { 27 | margin-bottom: 0; 28 | } 29 | 30 | .benchmark-label { 31 | display: flex; 32 | justify-content: space-between; 33 | align-items: center; 34 | margin-bottom: 0.75rem; 35 | } 36 | 37 | .benchmark-name { 38 | font-size: 1.25rem; 39 | font-weight: 700; 40 | color: var(--text-primary); 41 | } 42 | 43 | .benchmark-value { 44 | font-size: 1.5rem; 45 | font-weight: 800; 46 | font-family: 'Monaco', monospace; 47 | color: var(--accent-cyan); 48 | } 49 | 50 | .benchmark-bar-container { 51 | height: 60px; 52 | background: var(--bg-secondary); 53 | border-radius: 8px; 54 | overflow: hidden; 55 | position: relative; 56 | } 57 | 58 | .benchmark-bar { 59 | height: 100%; 60 | transition: width 1.5s cubic-bezier(0.65, 0, 0.35, 1); 61 | display: flex; 62 | align-items: center; 63 | justify-content: flex-end; 64 | padding-right: 1rem; 65 | position: relative; 66 | } 67 | 68 | .winner-badge { 69 | display: inline-flex; 70 | align-items: center; 71 | gap: 0.5rem; 72 | padding: 0.5rem 1rem; 73 | background: rgba(0, 0, 0, 0.5); 74 | border-radius: 20px; 75 | font-size: 0.875rem; 76 | font-weight: 700; 77 | color: var(--text-primary); 78 | backdrop-filter: blur(10px); 79 | } 80 | 81 | .comparison-grid { 82 | display: grid; 83 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 84 | gap: 2rem; 85 | max-width: 900px; 86 | margin: 0 auto; 87 | } 88 | 89 | .comparison-card { 90 | background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%); 91 | border: 1px solid rgba(0, 243, 255, 0.2); 92 | border-radius: 12px; 93 | padding: 2rem; 94 | text-align: center; 95 | transition: all 0.3s ease; 96 | } 97 | 98 | .comparison-card:hover { 99 | border-color: var(--accent-cyan); 100 | box-shadow: var(--glow-cyan); 101 | transform: translateY(-5px); 102 | } 103 | 104 | .comparison-icon { 105 | font-size: 3rem; 106 | margin-bottom: 1rem; 107 | filter: drop-shadow(0 0 10px rgba(0, 243, 255, 0.5)); 108 | } 109 | 110 | .comparison-title { 111 | font-size: 2rem; 112 | font-weight: 800; 113 | background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple)); 114 | -webkit-background-clip: text; 115 | -webkit-text-fill-color: transparent; 116 | background-clip: text; 117 | margin-bottom: 0.5rem; 118 | } 119 | 120 | .comparison-subtitle { 121 | font-size: 0.875rem; 122 | color: var(--text-tertiary); 123 | text-transform: uppercase; 124 | letter-spacing: 0.05em; 125 | } 126 | 127 | @media (max-width: 768px) { 128 | .benchmark-chart { 129 | padding: 2rem 1.5rem; 130 | } 131 | 132 | .benchmark-name { 133 | font-size: 1rem; 134 | } 135 | 136 | .benchmark-value { 137 | font-size: 1.125rem; 138 | } 139 | 140 | .comparison-grid { 141 | grid-template-columns: 1fr; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /website/src/components/Installation.css: -------------------------------------------------------------------------------- 1 | .installation { 2 | background: var(--bg-secondary); 3 | } 4 | 5 | .installation-content { 6 | max-width: 1000px; 7 | margin: 0 auto; 8 | } 9 | 10 | .platform-selector { 11 | margin-bottom: 3rem; 12 | } 13 | 14 | .selector-title { 15 | font-size: 1.5rem; 16 | font-weight: 700; 17 | margin-bottom: 1.5rem; 18 | color: var(--text-primary); 19 | } 20 | 21 | .platform-grid { 22 | display: grid; 23 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 24 | gap: 1rem; 25 | } 26 | 27 | .platform-btn { 28 | background: var(--bg-tertiary); 29 | border: 2px solid rgba(0, 243, 255, 0.2); 30 | border-radius: 8px; 31 | padding: 1rem; 32 | color: var(--text-primary); 33 | font-size: 0.95rem; 34 | font-weight: 600; 35 | cursor: pointer; 36 | transition: all 0.3s ease; 37 | text-align: left; 38 | position: relative; 39 | } 40 | 41 | .platform-btn:hover { 42 | border-color: var(--accent-cyan); 43 | background: rgba(0, 243, 255, 0.05); 44 | } 45 | 46 | .platform-btn.active { 47 | border-color: var(--accent-cyan); 48 | background: rgba(0, 243, 255, 0.1); 49 | box-shadow: var(--glow-cyan); 50 | } 51 | 52 | .recommended-badge { 53 | display: block; 54 | font-size: 0.75rem; 55 | color: var(--accent-yellow); 56 | margin-top: 0.5rem; 57 | } 58 | 59 | .code-block-container { 60 | background: var(--bg-tertiary); 61 | border: 1px solid rgba(0, 243, 255, 0.2); 62 | border-radius: 12px; 63 | overflow: hidden; 64 | margin-bottom: 2rem; 65 | } 66 | 67 | .code-block-header { 68 | display: flex; 69 | justify-content: space-between; 70 | align-items: center; 71 | padding: 1rem 1.5rem; 72 | background: rgba(0, 243, 255, 0.05); 73 | border-bottom: 1px solid rgba(0, 243, 255, 0.2); 74 | } 75 | 76 | .code-block-title { 77 | font-weight: 600; 78 | color: var(--accent-cyan); 79 | } 80 | 81 | .copy-btn { 82 | display: flex; 83 | align-items: center; 84 | gap: 0.5rem; 85 | padding: 0.5rem 1rem; 86 | background: rgba(0, 243, 255, 0.1); 87 | border: 1px solid var(--accent-cyan); 88 | border-radius: 6px; 89 | color: var(--accent-cyan); 90 | font-size: 0.875rem; 91 | font-weight: 600; 92 | cursor: pointer; 93 | transition: all 0.3s ease; 94 | } 95 | 96 | .copy-btn:hover { 97 | background: var(--accent-cyan); 98 | color: var(--bg-primary); 99 | box-shadow: var(--glow-cyan); 100 | } 101 | 102 | .code-block-container pre { 103 | margin: 0; 104 | padding: 1.5rem; 105 | background: transparent; 106 | border: none; 107 | box-shadow: none; 108 | } 109 | 110 | .docs-links { 111 | display: grid; 112 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 113 | gap: 1rem; 114 | } 115 | 116 | .doc-link { 117 | display: flex; 118 | align-items: center; 119 | gap: 0.75rem; 120 | padding: 1.25rem; 121 | background: var(--bg-tertiary); 122 | border: 1px solid rgba(0, 243, 255, 0.2); 123 | border-radius: 8px; 124 | color: var(--text-primary); 125 | text-decoration: none; 126 | font-weight: 600; 127 | transition: all 0.3s ease; 128 | } 129 | 130 | .doc-link svg { 131 | color: var(--accent-cyan); 132 | flex-shrink: 0; 133 | } 134 | 135 | .doc-link:hover { 136 | border-color: var(--accent-cyan); 137 | box-shadow: var(--glow-cyan); 138 | transform: translateY(-2px); 139 | } 140 | 141 | @media (max-width: 768px) { 142 | .platform-grid { 143 | grid-template-columns: 1fr; 144 | } 145 | 146 | .docs-links { 147 | grid-template-columns: 1fr; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /website/src/components/Performance.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react' 2 | import './Performance.css' 3 | 4 | export default function Performance() { 5 | const [isVisible, setIsVisible] = useState(false) 6 | const sectionRef = useRef(null) 7 | 8 | const benchmarks = [ 9 | { name: 'Floo', throughput: 30.9, color: 'var(--accent-cyan)' }, 10 | { name: 'Rathole', throughput: 16.6, color: 'var(--accent-purple)' }, 11 | { name: 'FRP', throughput: 9.2, color: 'var(--accent-pink)' } 12 | ] 13 | 14 | const maxThroughput = Math.max(...benchmarks.map(b => b.throughput)) 15 | 16 | useEffect(() => { 17 | const observer = new IntersectionObserver( 18 | ([entry]) => { 19 | if (entry.isIntersecting) { 20 | setIsVisible(true) 21 | } 22 | }, 23 | { threshold: 0.2 } 24 | ) 25 | 26 | if (sectionRef.current) { 27 | observer.observe(sectionRef.current) 28 | } 29 | 30 | return () => observer.disconnect() 31 | }, []) 32 | 33 | return ( 34 |
35 |
36 |

Performance

37 | 38 |
39 | Benchmark: Apple M1 (4 vCPU) | Plaintext | Single Stream | iperf3 40 |
41 | 42 |
43 | {benchmarks.map((bench, index) => ( 44 |
45 |
46 | {bench.name} 47 | {bench.throughput} Gbps 48 |
49 |
50 |
59 | {index === 0 && ( 60 |
61 | 62 | 63 | 64 | Champion 65 |
66 | )} 67 |
68 |
69 |
70 | ))} 71 |
72 | 73 |
74 |
75 |
76 |
86% Faster
77 |
Than Rathole
78 |
79 |
80 |
🚀
81 |
236% Faster
82 |
Than FRP
83 |
84 |
85 |
💾
86 |
671 KB
87 |
Combined Binary Size
88 |
89 |
90 |
91 |
92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /website/src/components/Comparison.css: -------------------------------------------------------------------------------- 1 | .comparison { 2 | background: var(--bg-secondary); 3 | } 4 | 5 | .comparison-subtitle { 6 | text-align: center; 7 | color: var(--text-secondary); 8 | font-size: 1.125rem; 9 | margin-bottom: 3rem; 10 | } 11 | 12 | .comparison-table-wrapper { 13 | overflow-x: auto; 14 | margin-bottom: 3rem; 15 | background: var(--bg-tertiary); 16 | border: 1px solid rgba(0, 243, 255, 0.2); 17 | border-radius: 16px; 18 | padding: 1rem; 19 | } 20 | 21 | .comparison-table { 22 | width: 100%; 23 | border-collapse: collapse; 24 | font-size: 0.9375rem; 25 | } 26 | 27 | .comparison-table thead { 28 | border-bottom: 2px solid rgba(0, 243, 255, 0.3); 29 | } 30 | 31 | .comparison-table th { 32 | padding: 1.25rem 1rem; 33 | text-align: left; 34 | font-weight: 700; 35 | color: var(--text-primary); 36 | text-transform: uppercase; 37 | font-size: 0.875rem; 38 | letter-spacing: 0.05em; 39 | } 40 | 41 | .comparison-table th.floo-column { 42 | color: var(--accent-cyan); 43 | text-shadow: 0 0 10px rgba(0, 243, 255, 0.5); 44 | } 45 | 46 | .comparison-table tbody tr { 47 | border-bottom: 1px solid rgba(0, 243, 255, 0.1); 48 | transition: background 0.3s ease; 49 | } 50 | 51 | .comparison-table tbody tr:hover { 52 | background: rgba(0, 243, 255, 0.05); 53 | } 54 | 55 | .comparison-table td { 56 | padding: 1rem; 57 | } 58 | 59 | .feature-name { 60 | font-weight: 600; 61 | color: var(--text-primary); 62 | white-space: nowrap; 63 | } 64 | 65 | .value-cell { 66 | color: var(--text-secondary); 67 | } 68 | 69 | .value-cell.highlight { 70 | color: var(--accent-cyan); 71 | font-weight: 600; 72 | background: rgba(0, 243, 255, 0.1); 73 | position: relative; 74 | } 75 | 76 | .comparison-highlights { 77 | display: grid; 78 | grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); 79 | gap: 1.5rem; 80 | margin-bottom: 2rem; 81 | } 82 | 83 | .highlight-card { 84 | background: linear-gradient(135deg, var(--bg-tertiary), var(--bg-secondary)); 85 | border: 1px solid rgba(0, 243, 255, 0.2); 86 | border-radius: 12px; 87 | padding: 2rem; 88 | text-align: center; 89 | transition: all 0.3s ease; 90 | } 91 | 92 | .highlight-card:hover { 93 | border-color: var(--accent-cyan); 94 | box-shadow: var(--glow-cyan); 95 | transform: translateY(-3px); 96 | } 97 | 98 | .highlight-icon { 99 | font-size: 2.5rem; 100 | margin-bottom: 1rem; 101 | filter: drop-shadow(0 0 10px rgba(0, 243, 255, 0.5)); 102 | } 103 | 104 | .highlight-title { 105 | font-size: 1.5rem; 106 | font-weight: 700; 107 | background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple)); 108 | -webkit-background-clip: text; 109 | -webkit-text-fill-color: transparent; 110 | background-clip: text; 111 | margin-bottom: 0.75rem; 112 | } 113 | 114 | .highlight-text { 115 | font-size: 0.9375rem; 116 | color: var(--text-secondary); 117 | line-height: 1.6; 118 | } 119 | 120 | .comparison-note { 121 | display: flex; 122 | align-items: flex-start; 123 | gap: 0.75rem; 124 | padding: 1.25rem; 125 | background: rgba(0, 243, 255, 0.05); 126 | border: 1px solid rgba(0, 243, 255, 0.2); 127 | border-radius: 8px; 128 | color: var(--text-tertiary); 129 | font-size: 0.875rem; 130 | line-height: 1.6; 131 | } 132 | 133 | .comparison-note svg { 134 | color: var(--accent-cyan); 135 | flex-shrink: 0; 136 | margin-top: 0.15rem; 137 | } 138 | 139 | @media (max-width: 768px) { 140 | .comparison-table { 141 | font-size: 0.875rem; 142 | } 143 | 144 | .comparison-table th, 145 | .comparison-table td { 146 | padding: 0.75rem 0.5rem; 147 | } 148 | 149 | .feature-name { 150 | white-space: normal; 151 | font-size: 0.875rem; 152 | } 153 | 154 | .highlight-title { 155 | font-size: 1.25rem; 156 | } 157 | 158 | .comparison-highlights { 159 | grid-template-columns: 1fr; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /packaging/setup-apt-repo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Set up APT repository structure and generate repository metadata 4 | # This script creates an APT repository that can be hosted on GitHub Pages 5 | # 6 | # Usage: ./setup-apt-repo.sh REPO_DIR VERSION 7 | # Example: ./setup-apt-repo.sh apt-repo 0.1.2 8 | 9 | set -e 10 | 11 | if [ -z "$1" ] || [ -z "$2" ]; then 12 | echo "Usage: $0 REPO_DIR VERSION" 13 | echo "Example: $0 apt-repo 0.1.2" 14 | exit 1 15 | fi 16 | 17 | REPO_DIR="$1" 18 | VERSION="$2" 19 | 20 | # Check for required tools 21 | if ! command -v dpkg-scanpackages &> /dev/null; then 22 | echo "Error: dpkg-scanpackages not found. Install with: apt-get install dpkg-dev" 23 | exit 1 24 | fi 25 | 26 | echo "Setting up APT repository in ${REPO_DIR}..." 27 | echo "" 28 | 29 | # Create repository structure 30 | mkdir -p "${REPO_DIR}/pool/main" 31 | mkdir -p "${REPO_DIR}/dists/stable/main/binary-amd64" 32 | mkdir -p "${REPO_DIR}/dists/stable/main/binary-arm64" 33 | 34 | # Copy .deb packages 35 | echo "Copying .deb packages..." 36 | cp build-deb/floo_${VERSION}-1_amd64.deb "${REPO_DIR}/pool/main/" 37 | cp build-deb/floo_${VERSION}-1_arm64.deb "${REPO_DIR}/pool/main/" 38 | 39 | # Generate Packages files 40 | echo "Generating Packages files..." 41 | cd "${REPO_DIR}" 42 | 43 | # amd64 44 | dpkg-scanpackages --arch amd64 pool/ > dists/stable/main/binary-amd64/Packages 45 | gzip -k -f dists/stable/main/binary-amd64/Packages 46 | 47 | # arm64 48 | dpkg-scanpackages --arch arm64 pool/ > dists/stable/main/binary-arm64/Packages 49 | gzip -k -f dists/stable/main/binary-arm64/Packages 50 | 51 | # Generate Release file 52 | cat > dists/stable/Release << EOF 53 | Origin: Floo 54 | Label: Floo 55 | Suite: stable 56 | Codename: stable 57 | Version: 1.0 58 | Architectures: amd64 arm64 59 | Components: main 60 | Description: Floo APT Repository - High-performance tunneling in Zig 61 | Date: $(date -R) 62 | EOF 63 | 64 | # Calculate checksums for Release file 65 | echo "MD5Sum:" >> dists/stable/Release 66 | find dists/stable/main -type f -exec md5sum {} \; | sed 's|dists/stable/||' >> dists/stable/Release 67 | 68 | echo "SHA1:" >> dists/stable/Release 69 | find dists/stable/main -type f -exec sha1sum {} \; | sed 's|dists/stable/||' >> dists/stable/Release 70 | 71 | echo "SHA256:" >> dists/stable/Release 72 | find dists/stable/main -type f -exec sha256sum {} \; | sed 's|dists/stable/||' >> dists/stable/Release 73 | 74 | cd .. 75 | 76 | echo "" 77 | echo "=== APT Repository Created ===" 78 | echo "Location: ${REPO_DIR}" 79 | echo "" 80 | echo "Repository structure:" 81 | tree "${REPO_DIR}" 2>/dev/null || find "${REPO_DIR}" -type f 82 | 83 | echo "" 84 | echo "=== Next Steps ===" 85 | echo "" 86 | echo "Option 1: Host on GitHub Pages" 87 | echo " 1. Create a new repo: floo-apt" 88 | echo " 2. Copy ${REPO_DIR}/* to the repo" 89 | echo " 3. Enable GitHub Pages (Settings → Pages → Branch: main)" 90 | echo " 4. Users add with:" 91 | echo " curl -fsSL https://USERNAME.github.io/floo-apt/pubkey.gpg | sudo gpg --dearmor -o /usr/share/keyrings/floo.gpg" 92 | echo " echo 'deb [signed-by=/usr/share/keyrings/floo.gpg] https://USERNAME.github.io/floo-apt stable main' | sudo tee /etc/apt/sources.list.d/floo.list" 93 | echo " sudo apt update" 94 | echo " sudo apt install floo" 95 | echo "" 96 | echo "Option 2: Sign with GPG (Recommended for production)" 97 | echo " 1. Generate GPG key:" 98 | echo " gpg --full-generate-key" 99 | echo " 2. Sign Release file:" 100 | echo " cd ${REPO_DIR}/dists/stable" 101 | echo " gpg --default-key YOUR_KEY_ID -abs -o Release.gpg Release" 102 | echo " gpg --default-key YOUR_KEY_ID --clearsign -o InRelease Release" 103 | echo " 3. Export public key:" 104 | echo " gpg --armor --export YOUR_KEY_ID > ${REPO_DIR}/pubkey.gpg" 105 | echo "" 106 | echo "Option 3: Use cloudsmith.io or packagecloud.io" 107 | echo " These services provide free APT hosting for open source projects" 108 | -------------------------------------------------------------------------------- /website/src/components/Hero.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import './Hero.css' 3 | 4 | export default function Hero() { 5 | const [typedText, setTypedText] = useState('') 6 | const fullText = '30.9 Gbps' 7 | 8 | useEffect(() => { 9 | let index = 0 10 | const timer = setInterval(() => { 11 | if (index <= fullText.length) { 12 | setTypedText(fullText.slice(0, index)) 13 | index++ 14 | } else { 15 | clearInterval(timer) 16 | } 17 | }, 100) 18 | return () => clearInterval(timer) 19 | }, []) 20 | 21 | return ( 22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 |
30 |
31 | FLOO 32 |
33 | 34 |
35 | 36 | Powered by Zig 37 |
38 | 39 |

40 | High-Throughput 41 |
42 | Tunneling 43 |

44 | 45 |

46 | Zero dependencies. Maximum performance. 47 |

48 | 49 |
50 |
51 | {typedText}| 52 |
53 |
Peak Throughput
54 |
55 | 56 |
57 |
58 |
0
59 |
Dependencies
60 |
61 |
62 |
671 KB
63 |
Total Binary Size
64 |
65 |
66 |
86%
67 |
Faster than Rathole
68 |
69 |
70 |
236%
71 |
Faster than FRP
72 |
73 |
74 | 75 | 90 |
91 |
92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /website/src/components/Comparison.jsx: -------------------------------------------------------------------------------- 1 | import './Comparison.css' 2 | 3 | export default function Comparison() { 4 | const comparisonData = [ 5 | { feature: 'Language', floo: 'Zig', rathole: 'Rust', frp: 'Go' }, 6 | { feature: 'Dependencies', floo: '0 ⭐', rathole: '27+ crates', frp: '34+ packages', highlight: 'floo' }, 7 | { feature: 'Max Throughput (M1)', floo: '29.4 Gbps ⭐', rathole: '18.1 Gbps', frp: '10.0 Gbps', highlight: 'floo' }, 8 | { feature: 'Binary Size', floo: '671 KB ⭐', rathole: '~2-4 MB', frp: '~24+ MB', highlight: 'floo' }, 9 | { feature: 'Encryption', floo: 'Noise XX + PSK', rathole: 'Noise NK, TLS', frp: 'TLS' }, 10 | { feature: 'Ciphers', floo: '5 AEAD', rathole: 'ChaCha20-Poly1305', frp: 'TLS standard' }, 11 | { feature: 'Parallel Tunnels', floo: '✅ Round-robin (1-16)', rathole: '🔶 Not documented', frp: '✅ Connection pool' }, 12 | { feature: 'Hot Config Reload', floo: '🔶 Restart (planned)', rathole: '✅ Dynamic services', frp: '✅ Admin API' }, 13 | { feature: 'Built-in Diagnostics', floo: '✅ --doctor, --ping', rathole: '🔶 Logging only', frp: '✅ Dashboard, Prometheus' }, 14 | { feature: 'Proxy Client', floo: '✅ SOCKS5, HTTP', rathole: '✅ SOCKS5, HTTP', frp: '✅ HTTP, SOCKS5' }, 15 | ] 16 | 17 | return ( 18 |
19 |
20 |

Feature Comparison

21 |

22 | How Floo stacks up against similar tools 23 |

24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {comparisonData.map((row, index) => ( 37 | 38 | 39 | 42 | 43 | 44 | 45 | ))} 46 | 47 |
FeatureFlooRatholeFRP
{row.feature} 40 | {row.floo} 41 | {row.rathole}{row.frp}
48 |
49 | 50 |
51 |
52 |
🎯
53 |
Zero Dependencies
54 |
55 | Only Zig stdlib - no supply chain vulnerabilities 56 |
57 |
58 |
59 |
60 |
62% Faster
61 |
62 | Outperforms Rathole with AEGIS-128L cipher 63 |
64 |
65 |
66 |
📦
67 |
Smallest Binaries
68 |
69 | 671 KB total vs 2-4 MB (Rathole) or 24+ MB (FRP) 70 |
71 |
72 |
73 | 74 |
75 | 76 | 77 | 78 | All features verified against source repositories (Rathole v0.5.0, FRP v0.65.0). 79 | Benchmarks on identical hardware (Apple M1 MacBook Air) using iperf3. 80 |
81 |
82 |
83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /website/src/components/GitHub.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import './GitHub.css' 3 | 4 | export default function GitHub() { 5 | const [repoData, setRepoData] = useState(null) 6 | const [latestRelease, setLatestRelease] = useState(null) 7 | 8 | useEffect(() => { 9 | // Fetch repository data 10 | fetch('https://api.github.com/repos/YUX/floo') 11 | .then(res => res.json()) 12 | .then(data => setRepoData(data)) 13 | .catch(err => console.error('Failed to fetch repo data:', err)) 14 | 15 | // Fetch latest release 16 | fetch('https://api.github.com/repos/YUX/floo/releases/latest') 17 | .then(res => res.json()) 18 | .then(data => setLatestRelease(data)) 19 | .catch(err => console.error('Failed to fetch release data:', err)) 20 | }, []) 21 | 22 | return ( 23 |
24 |
25 |

Open Source

26 | 27 |
28 | {repoData && ( 29 |
30 |
31 |

GitHub Stats

32 |
33 |
34 |
{repoData.stargazers_count?.toLocaleString() || 0}
35 |
Stars
36 |
37 |
38 |
{repoData.forks_count?.toLocaleString() || 0}
39 |
Forks
40 |
41 |
42 |
{repoData.open_issues_count?.toLocaleString() || 0}
43 |
Issues
44 |
45 |
46 | 47 | View Repository → 48 | 49 |
50 | )} 51 | 52 | {latestRelease && latestRelease.tag_name && ( 53 |
54 |
🚀
55 |

Latest Release

56 |
57 |
{latestRelease.tag_name}
58 |
59 | {new Date(latestRelease.published_at).toLocaleDateString('en-US', { 60 | year: 'numeric', 61 | month: 'long', 62 | day: 'numeric' 63 | })} 64 |
65 |
66 | 67 | View Release → 68 | 69 |
70 | )} 71 | 72 |
73 |
💡
74 |

Contribute

75 |

76 | Floo is open source and welcomes contributions. Report bugs, suggest features, or submit pull requests. 77 |

78 | 79 | Open an Issue → 80 | 81 |
82 |
83 | 84 |
85 | 86 | 87 | 88 | Licensed under MIT License 89 |
90 |
91 |
92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /packaging/build-deb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Build .deb packages from GitHub release artifacts 4 | # Usage: ./build-deb.sh VERSION 5 | # Example: ./build-deb.sh 0.1.2 6 | 7 | set -e 8 | 9 | if [ -z "$1" ]; then 10 | echo "Usage: $0 VERSION" 11 | echo "Example: $0 0.1.2" 12 | exit 1 13 | fi 14 | 15 | VERSION=$1 16 | RELEASE_TAG="v${VERSION}" 17 | BASE_URL="https://github.com/YUX/floo/releases/download/${RELEASE_TAG}" 18 | 19 | # Create build directory 20 | BUILD_DIR="build-deb" 21 | rm -rf "$BUILD_DIR" 22 | mkdir -p "$BUILD_DIR" 23 | cd "$BUILD_DIR" 24 | 25 | echo "Building .deb packages for Floo v${VERSION}..." 26 | echo "" 27 | 28 | # Build for amd64 (x86_64 Haswell) 29 | echo "=== Building amd64 package ===" 30 | ARCH="amd64" 31 | ARTIFACT="floo-x86_64-linux-gnu-haswell.tar.gz" 32 | PACKAGE_DIR="floo_${VERSION}-1_${ARCH}" 33 | 34 | mkdir -p "$PACKAGE_DIR" 35 | cd "$PACKAGE_DIR" 36 | 37 | # Download and extract 38 | wget -q "${BASE_URL}/${ARTIFACT}" 39 | tar xzf "$ARTIFACT" 40 | mv "x86_64-linux-gnu-haswell"/* . 41 | rm "$ARTIFACT" 42 | 43 | # Create debian package structure 44 | mkdir -p DEBIAN 45 | mkdir -p usr/bin 46 | mkdir -p usr/share/doc/floo/examples 47 | 48 | # Move binaries 49 | mv flooc usr/bin/ 50 | mv floos usr/bin/ 51 | chmod 755 usr/bin/flooc usr/bin/floos 52 | 53 | # Move documentation 54 | mv README.md usr/share/doc/floo/ 55 | mv flooc.toml.example usr/share/doc/floo/examples/ 56 | mv floos.toml.example usr/share/doc/floo/examples/ 57 | 58 | # Create control file 59 | cat > DEBIAN/control << EOF 60 | Package: floo 61 | Version: ${VERSION}-1 62 | Section: net 63 | Priority: optional 64 | Architecture: ${ARCH} 65 | Maintainer: Your Name 66 | Homepage: https://github.com/YUX/floo 67 | Description: Secure, high-performance tunneling in Zig 68 | Floo is a high-throughput tunneling solution written in Zig with zero 69 | dependencies. It provides secure tunneling using Noise XX protocol with 70 | multiple AEAD ciphers. 71 | . 72 | Features: 73 | - 29.4 Gbps throughput with AEGIS-128L cipher 74 | - Zero runtime dependencies 75 | - Reverse and forward tunneling modes 76 | - SOCKS5 and HTTP CONNECT proxy support 77 | - Config changes applied on restart (SIGHUP reload temporarily disabled) 78 | - Built-in diagnostics (--doctor, --ping) 79 | EOF 80 | 81 | # Build package 82 | cd .. 83 | dpkg-deb --build "$PACKAGE_DIR" 84 | 85 | echo "✓ Built: ${PACKAGE_DIR}.deb" 86 | echo "" 87 | 88 | # Build for arm64 (aarch64) 89 | echo "=== Building arm64 package ===" 90 | ARCH="arm64" 91 | ARTIFACT="floo-aarch64-linux-gnu.tar.gz" 92 | PACKAGE_DIR="floo_${VERSION}-1_${ARCH}" 93 | 94 | mkdir -p "$PACKAGE_DIR" 95 | cd "$PACKAGE_DIR" 96 | 97 | # Download and extract 98 | wget -q "${BASE_URL}/${ARTIFACT}" 99 | tar xzf "$ARTIFACT" 100 | mv "aarch64-linux-gnu"/* . 101 | rm "$ARTIFACT" 102 | 103 | # Create debian package structure 104 | mkdir -p DEBIAN 105 | mkdir -p usr/bin 106 | mkdir -p usr/share/doc/floo/examples 107 | 108 | # Move binaries 109 | mv flooc usr/bin/ 110 | mv floos usr/bin/ 111 | chmod 755 usr/bin/flooc usr/bin/floos 112 | 113 | # Move documentation 114 | mv README.md usr/share/doc/floo/ 115 | mv flooc.toml.example usr/share/doc/floo/examples/ 116 | mv floos.toml.example usr/share/doc/floo/examples/ 117 | 118 | # Create control file 119 | cat > DEBIAN/control << EOF 120 | Package: floo 121 | Version: ${VERSION}-1 122 | Section: net 123 | Priority: optional 124 | Architecture: ${ARCH} 125 | Maintainer: Your Name 126 | Homepage: https://github.com/YUX/floo 127 | Description: Secure, high-performance tunneling in Zig 128 | Floo is a high-throughput tunneling solution written in Zig with zero 129 | dependencies. It provides secure tunneling using Noise XX protocol with 130 | multiple AEAD ciphers. 131 | . 132 | Features: 133 | - 29.4 Gbps throughput with AEGIS-128L cipher 134 | - Zero runtime dependencies 135 | - Reverse and forward tunneling modes 136 | - SOCKS5 and HTTP CONNECT proxy support 137 | - Config changes applied on restart (SIGHUP reload temporarily disabled) 138 | - Built-in diagnostics (--doctor, --ping) 139 | EOF 140 | 141 | # Build package 142 | cd .. 143 | dpkg-deb --build "$PACKAGE_DIR" 144 | 145 | echo "✓ Built: ${PACKAGE_DIR}.deb" 146 | echo "" 147 | 148 | cd .. 149 | 150 | echo "=== Summary ===" 151 | echo "Packages built in ${BUILD_DIR}/:" 152 | ls -lh "${BUILD_DIR}"/*.deb 153 | 154 | echo "" 155 | echo "To install locally:" 156 | echo " sudo dpkg -i ${BUILD_DIR}/floo_${VERSION}-1_amd64.deb" 157 | echo "" 158 | echo "To create APT repository, see: packaging/setup-apt-repo.sh" 159 | -------------------------------------------------------------------------------- /packaging/snap/README.md: -------------------------------------------------------------------------------- 1 | # Floo Snap Package 2 | 3 | Official Snap package for Floo - High-performance tunneling in Zig. 4 | 5 | ## Installation 6 | 7 | ### From Snap Store (after publishing) 8 | 9 | ```bash 10 | sudo snap install floo 11 | ``` 12 | 13 | ### From Local Build 14 | 15 | ```bash 16 | # Build the snap 17 | cd packaging 18 | ./build-snap.sh 0.1.2 19 | 20 | # Install 21 | sudo snap install snap/floo_0.1.2_amd64.snap --dangerous 22 | ``` 23 | 24 | ## Usage 25 | 26 | ### Running Commands 27 | 28 | ```bash 29 | # Client 30 | floo.flooc --version 31 | floo.flooc flooc.toml 32 | 33 | # Server 34 | floo.floos --version 35 | floo.floos floos.toml 36 | ``` 37 | 38 | ### Configuration Files 39 | 40 | Example configs are located at: 41 | ``` 42 | /snap/floo/current/share/doc/floo/examples/ 43 | ``` 44 | 45 | Copy them to your home directory: 46 | ```bash 47 | cp /snap/floo/current/share/doc/floo/examples/*.toml.example ~/ 48 | ``` 49 | 50 | ### Running as a Service (Server) 51 | 52 | The snap includes a daemon for running floos as a system service: 53 | 54 | ```bash 55 | # The service uses a default config at /var/snap/floo/common/floos.toml 56 | # Create your config: 57 | sudo mkdir -p /var/snap/floo/common 58 | sudo nano /var/snap/floo/common/floos.toml 59 | 60 | # Enable and start the service 61 | sudo snap start floo.floos 62 | sudo snap logs floo.floos 63 | 64 | # Check status 65 | sudo snap services floo.floos 66 | 67 | # Stop the service 68 | sudo snap stop floo.floos 69 | ``` 70 | 71 | ## Permissions 72 | 73 | The snap uses strict confinement with the following interfaces: 74 | 75 | - **network** - Required for network connections 76 | - **network-bind** - Required to bind to ports 77 | - **network-control** - Required for advanced networking (server only) 78 | - **home** - Read access to home directory for config files 79 | 80 | All interfaces are connected automatically on installation. 81 | 82 | ## Building Locally 83 | 84 | ### Prerequisites 85 | 86 | ```bash 87 | # Install snapcraft 88 | sudo snap install snapcraft --classic 89 | 90 | # Install LXD (for building) 91 | sudo snap install lxd 92 | sudo lxd init --auto 93 | ``` 94 | 95 | ### Build 96 | 97 | ```bash 98 | cd packaging/snap 99 | snapcraft 100 | ``` 101 | 102 | This creates `floo_VERSION_ARCH.snap` in the packaging/snap directory. 103 | 104 | ## Publishing to Snap Store 105 | 106 | ### One-Time Setup 107 | 108 | 1. **Register the snap name:** 109 | ```bash 110 | snapcraft register floo 111 | ``` 112 | 113 | 2. **Login to Snapcraft:** 114 | ```bash 115 | snapcraft login 116 | ``` 117 | 118 | 3. **Export credentials for CI/CD:** 119 | ```bash 120 | snapcraft export-login --snaps=floo --channels=stable snapcraft-token.txt 121 | # Add contents as SNAPCRAFT_TOKEN secret in GitHub 122 | ``` 123 | 124 | ### Manual Publishing 125 | 126 | ```bash 127 | cd packaging/snap 128 | 129 | # Upload and release to stable channel 130 | snapcraft upload floo_0.1.2_amd64.snap --release=stable 131 | 132 | # Or upload to edge for testing first 133 | snapcraft upload floo_0.1.2_amd64.snap --release=edge 134 | ``` 135 | 136 | ### Automated Publishing 137 | 138 | The GitHub Actions workflow `.github/workflows/publish-snap.yml` automatically: 139 | - Builds snap for amd64 and arm64 140 | - Publishes to Snap Store on each release (if SNAPCRAFT_TOKEN is set) 141 | 142 | ## Architecture Support 143 | 144 | - **amd64** (x86_64) - Uses Haswell-optimized build 145 | - **arm64** (aarch64) - Uses baseline ARM64 build 146 | 147 | ## Troubleshooting 148 | 149 | ### Permission Denied 150 | 151 | If you get permission errors accessing config files: 152 | 153 | ```bash 154 | # Check snap connections 155 | snap connections floo 156 | 157 | # Manually connect home interface if needed 158 | sudo snap connect floo:home 159 | ``` 160 | 161 | ### Service Not Starting 162 | 163 | ```bash 164 | # Check logs 165 | sudo snap logs floo.floos -f 166 | 167 | # Verify config file exists and is valid 168 | sudo ls -la /var/snap/floo/common/floos.toml 169 | floo.floos --doctor /var/snap/floo/common/floos.toml 170 | ``` 171 | 172 | ### Port Already in Use 173 | 174 | The snap runs in a confined environment. Ensure no other services are using the same ports. 175 | 176 | ## Differences from Native Install 177 | 178 | - Commands are prefixed: `floo.flooc` and `floo.floos` (can create aliases) 179 | - Config files should be in home directory or `/var/snap/floo/common/` 180 | - Confined environment (strict confinement) for security 181 | 182 | ## Learn More 183 | 184 | - [Snapcraft Documentation](https://snapcraft.io/docs) 185 | - [Floo Documentation](https://github.com/YUX/floo) 186 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: Nightly Build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | nightly: 10 | name: Build and Release Nightly 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Setup Zig 22 | uses: goto-bus-stop/setup-zig@v2 23 | with: 24 | version: master 25 | 26 | - name: Build all platforms 27 | run: | 28 | zig build release-all 29 | 30 | - name: Package artifacts 31 | env: 32 | RELEASE_TARGETS: | 33 | x86_64-linux-gnu 34 | x86_64-linux-gnu-haswell 35 | x86_64-linux-musl 36 | aarch64-linux-gnu 37 | aarch64-linux-gnu-neoverse-n1 38 | aarch64-linux-gnu-rpi4 39 | x86_64-macos 40 | x86_64-macos-haswell 41 | aarch64-macos-m1 42 | run: | 43 | set -euo pipefail 44 | mkdir -p release-assets 45 | 46 | for target in $RELEASE_TARGETS; do 47 | src="zig-out/release/${target}" 48 | if [ ! -d "$src" ]; then 49 | echo "Skipping missing ${target}" 50 | continue 51 | fi 52 | mkdir -p "dist/${target}" 53 | 54 | for bin in flooc floos; do 55 | if [ -f "$src/${bin}.exe" ]; then 56 | cp "$src/${bin}.exe" "dist/${target}/${bin}.exe" 57 | elif [ -f "$src/${bin}" ]; then 58 | cp "$src/${bin}" "dist/${target}/${bin}" 59 | fi 60 | done 61 | 62 | cp README.md "dist/${target}/" 63 | cp configs/flooc.example.toml "dist/${target}/flooc.toml.example" 64 | cp configs/floos.example.toml "dist/${target}/floos.toml.example" 65 | 66 | if [[ "$target" == *"windows"* ]]; then 67 | (cd dist && zip -rq "../release-assets/floo-${target}.zip" "${target}") 68 | else 69 | (cd dist && tar czf "../release-assets/floo-${target}.tar.gz" "${target}") 70 | fi 71 | done 72 | 73 | ls -lh release-assets/ 74 | 75 | - name: Delete old nightly release and tag 76 | run: | 77 | gh release delete nightly --yes || true 78 | git push origin :refs/tags/nightly || true 79 | git tag -d nightly || true 80 | env: 81 | GH_TOKEN: ${{ github.token }} 82 | 83 | - name: Create new nightly tag 84 | run: | 85 | git tag nightly 86 | git push origin nightly 87 | 88 | - name: Create nightly release 89 | run: | 90 | gh release create nightly \ 91 | --title "Nightly Build ($(date +'%Y-%m-%d %H:%M UTC'))" \ 92 | --notes "**Automated nightly build from latest commit** 93 | 94 | ## Latest Changes 95 | 96 | Commit: \`${{ github.sha }}\` 97 | 98 | $(git log -1 --pretty=format:'**%s**%n%n%b') 99 | 100 | --- 101 | 102 | ### 🚀 Performance 103 | - **29.4 Gbps** encrypted throughput (AEGIS-128L) 104 | - **671 KB** total binary size 105 | - **Zero dependencies** 106 | 107 | ### 📥 Downloads 108 | 109 | | Platform | File | Recommended For | 110 | |----------|------|-----------------| 111 | | **Linux x86_64 (glibc)** | \`floo-x86_64-linux-gnu.tar.gz\` | Generic compatibility | 112 | | **Linux x86_64 (Haswell+)** | \`floo-x86_64-linux-gnu-haswell.tar.gz\` | Modern Intel/AMD (AES-NI) | 113 | | **Linux x86_64 (musl)** | \`floo-x86_64-linux-musl.tar.gz\` | Alpine/static containers | 114 | | **Linux ARM64 (generic)** | \`floo-aarch64-linux-gnu.tar.gz\` | ARM servers and SBCs | 115 | | **Linux ARM64 (Neoverse)** | \`floo-aarch64-linux-gnu-neoverse-n1.tar.gz\` | AWS Graviton / Ampere | 116 | | **Linux ARM64 (Raspberry Pi 4/5)** | \`floo-aarch64-linux-gnu-rpi4.tar.gz\` | Pi 4/5 tuned | 117 | | **macOS Apple Silicon** | \`floo-aarch64-macos-m1.tar.gz\` | M-series Macs | 118 | | **macOS Intel** | \`floo-x86_64-macos.tar.gz\` | Legacy Intel Macs | 119 | | **macOS Intel (Haswell+)** | \`floo-x86_64-macos-haswell.tar.gz\` | Best perf on 2013+ Macs | 120 | 121 | ### ⚠️ Note 122 | 123 | This is a **pre-release nightly build** from the latest code. 124 | - Updated automatically on every push to main 125 | - For production, use numbered releases (v0.1.0, v0.2.0, etc.) 126 | - Binaries are built with \`ReleaseFast\` optimization 127 | 128 | ### 🔒 Security 129 | 130 | Before using: 131 | 1. Replace placeholder PSK/tokens in config files 132 | 2. Run \`--doctor\` to validate setup 133 | 3. Use AEGIS-128L or AES-256-GCM cipher 134 | 135 | --- 136 | 137 | **Built from:** ${{ github.repository }}@${{ github.sha }} 138 | " \ 139 | --prerelease \ 140 | release-assets/* 141 | env: 142 | GH_TOKEN: ${{ github.token }} 143 | -------------------------------------------------------------------------------- /website/src/components/Installation.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import './Installation.css' 3 | 4 | export default function Installation() { 5 | const [selectedPlatform, setSelectedPlatform] = useState('linux-haswell') 6 | const [copied, setCopied] = useState(false) 7 | 8 | const platforms = [ 9 | { id: 'linux-haswell', name: 'Linux x86_64 (Haswell+)', file: 'floo-x86_64-linux-gnu-haswell.tar.gz', recommended: true }, 10 | { id: 'linux-baseline', name: 'Linux x86_64', file: 'floo-x86_64-linux-gnu.tar.gz' }, 11 | { id: 'linux-arm', name: 'Linux ARM64', file: 'floo-aarch64-linux-gnu.tar.gz' }, 12 | { id: 'macos-m1', name: 'macOS Apple Silicon (M1+)', file: 'floo-aarch64-macos-m1.tar.gz', recommended: true }, 13 | { id: 'macos-arm', name: 'macOS Apple Silicon', file: 'floo-aarch64-macos.tar.gz' }, 14 | { id: 'macos-haswell', name: 'macOS Intel (Haswell+)', file: 'floo-x86_64-macos-haswell.tar.gz', recommended: true }, 15 | { id: 'macos-intel', name: 'macOS Intel', file: 'floo-x86_64-macos.tar.gz' }, 16 | ] 17 | 18 | const selectedFile = platforms.find(p => p.id === selectedPlatform)?.file 19 | 20 | const installCommand = `# Download and extract 21 | curl -LO https://github.com/YUX/floo/releases/latest/download/${selectedFile} 22 | tar xzf ${selectedFile} 23 | cd ${selectedFile.replace('.tar.gz', '')} 24 | 25 | # Make binaries executable 26 | chmod +x flooc floos 27 | 28 | # Test the binaries 29 | ./flooc --version 30 | ./floos --version` 31 | 32 | const copyToClipboard = () => { 33 | navigator.clipboard.writeText(installCommand) 34 | setCopied(true) 35 | setTimeout(() => setCopied(false), 2000) 36 | } 37 | 38 | return ( 39 |
40 |
41 |

Installation

42 | 43 |
44 |
45 |

Choose Your Platform

46 |
47 | {platforms.map(platform => ( 48 | 58 | ))} 59 |
60 |
61 | 62 |
63 |
64 | Installation Commands 65 | 83 |
84 |
{installCommand}
85 |
86 | 87 | 107 |
108 |
109 |
110 | ) 111 | } 112 | -------------------------------------------------------------------------------- /website/src/components/Hero.css: -------------------------------------------------------------------------------- 1 | .hero { 2 | min-height: 100vh; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | position: relative; 7 | overflow: hidden; 8 | } 9 | 10 | .hero-bg { 11 | position: absolute; 12 | top: 0; 13 | left: 0; 14 | width: 100%; 15 | height: 100%; 16 | opacity: 0.3; 17 | } 18 | 19 | .circuit-line { 20 | position: absolute; 21 | background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent); 22 | height: 2px; 23 | width: 100%; 24 | animation: circuit-flow 3s linear infinite; 25 | } 26 | 27 | .circuit-1 { 28 | top: 20%; 29 | animation-delay: 0s; 30 | } 31 | 32 | .circuit-2 { 33 | top: 50%; 34 | animation-delay: 1s; 35 | } 36 | 37 | .circuit-3 { 38 | top: 80%; 39 | animation-delay: 2s; 40 | } 41 | 42 | @keyframes circuit-flow { 43 | 0% { 44 | transform: translateX(-100%); 45 | } 46 | 100% { 47 | transform: translateX(100%); 48 | } 49 | } 50 | 51 | .hero-content { 52 | text-align: center; 53 | z-index: 2; 54 | } 55 | 56 | .hero-logo { 57 | margin-bottom: 2rem; 58 | animation: fade-in-down 1s ease-out; 59 | } 60 | 61 | @keyframes fade-in-down { 62 | from { 63 | opacity: 0; 64 | transform: translateY(-30px); 65 | } 66 | to { 67 | opacity: 1; 68 | transform: translateY(0); 69 | } 70 | } 71 | 72 | .logo-floo { 73 | font-size: 6rem; 74 | font-weight: 400; 75 | font-family: 'UnifrakturMaguntia', 'Old English Text MT', 'Blackletter', serif; 76 | letter-spacing: 0.15em; 77 | background: linear-gradient(135deg, var(--accent-cyan) 0%, var(--accent-purple) 50%, var(--accent-pink) 100%); 78 | -webkit-background-clip: text; 79 | -webkit-text-fill-color: transparent; 80 | background-clip: text; 81 | text-shadow: 0 0 80px rgba(0, 243, 255, 0.5); 82 | filter: drop-shadow(0 0 30px rgba(0, 243, 255, 0.6)); 83 | display: inline-block; 84 | animation: logo-glow 3s ease-in-out infinite; 85 | position: relative; 86 | } 87 | 88 | @keyframes logo-glow { 89 | 0%, 100% { 90 | filter: drop-shadow(0 0 20px rgba(0, 243, 255, 0.6)); 91 | } 92 | 50% { 93 | filter: drop-shadow(0 0 40px rgba(139, 92, 246, 0.8)); 94 | } 95 | } 96 | 97 | .hero-badge { 98 | display: inline-flex; 99 | align-items: center; 100 | gap: 0.5rem; 101 | padding: 0.5rem 1rem; 102 | background: rgba(0, 243, 255, 0.1); 103 | border: 1px solid var(--accent-cyan); 104 | border-radius: 20px; 105 | font-size: 0.875rem; 106 | font-weight: 600; 107 | color: var(--accent-cyan); 108 | margin-bottom: 2rem; 109 | animation: float 3s ease-in-out infinite; 110 | } 111 | 112 | .pulse-dot { 113 | width: 8px; 114 | height: 8px; 115 | background: var(--accent-cyan); 116 | border-radius: 50%; 117 | animation: pulse 2s ease-in-out infinite; 118 | box-shadow: var(--glow-cyan); 119 | } 120 | 121 | @keyframes pulse { 122 | 0%, 100% { 123 | opacity: 1; 124 | } 125 | 50% { 126 | opacity: 0.5; 127 | } 128 | } 129 | 130 | @keyframes float { 131 | 0%, 100% { 132 | transform: translateY(0); 133 | } 134 | 50% { 135 | transform: translateY(-10px); 136 | } 137 | } 138 | 139 | .hero-title { 140 | font-size: 5rem; 141 | font-weight: 800; 142 | line-height: 1.1; 143 | margin-bottom: 1rem; 144 | letter-spacing: -0.02em; 145 | } 146 | 147 | .gradient-text { 148 | background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple), var(--accent-pink)); 149 | -webkit-background-clip: text; 150 | -webkit-text-fill-color: transparent; 151 | background-clip: text; 152 | animation: gradient-shift 3s ease infinite; 153 | background-size: 200% 200%; 154 | } 155 | 156 | @keyframes gradient-shift { 157 | 0%, 100% { 158 | background-position: 0% 50%; 159 | } 160 | 50% { 161 | background-position: 100% 50%; 162 | } 163 | } 164 | 165 | .hero-subtitle { 166 | font-size: 1.5rem; 167 | color: var(--text-secondary); 168 | margin-bottom: 3rem; 169 | } 170 | 171 | .hero-metric { 172 | margin-bottom: 3rem; 173 | } 174 | 175 | .metric-value { 176 | font-size: 4rem; 177 | font-weight: 800; 178 | color: var(--accent-cyan); 179 | text-shadow: var(--glow-cyan); 180 | font-family: 'Monaco', monospace; 181 | margin-bottom: 0.5rem; 182 | } 183 | 184 | .cursor { 185 | color: var(--accent-pink); 186 | animation: blink 1s step-end infinite; 187 | } 188 | 189 | @keyframes blink { 190 | 50% { 191 | opacity: 0; 192 | } 193 | } 194 | 195 | .metric-label { 196 | font-size: 1.125rem; 197 | color: var(--text-tertiary); 198 | text-transform: uppercase; 199 | letter-spacing: 0.1em; 200 | } 201 | 202 | .hero-stats { 203 | display: grid; 204 | grid-template-columns: repeat(4, 1fr); 205 | gap: 2rem; 206 | margin: 3rem 0; 207 | padding: 3rem; 208 | background: rgba(19, 19, 26, 0.6); 209 | border: 1px solid rgba(0, 243, 255, 0.2); 210 | border-radius: 16px; 211 | backdrop-filter: blur(10px); 212 | } 213 | 214 | .stat { 215 | text-align: center; 216 | } 217 | 218 | .stat-value { 219 | font-size: 2.5rem; 220 | font-weight: 700; 221 | background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple)); 222 | -webkit-background-clip: text; 223 | -webkit-text-fill-color: transparent; 224 | background-clip: text; 225 | margin-bottom: 0.5rem; 226 | } 227 | 228 | .stat-label { 229 | font-size: 0.875rem; 230 | color: var(--text-tertiary); 231 | text-transform: uppercase; 232 | letter-spacing: 0.05em; 233 | } 234 | 235 | .hero-actions { 236 | display: flex; 237 | gap: 1rem; 238 | justify-content: center; 239 | flex-wrap: wrap; 240 | margin-top: 2rem; 241 | } 242 | 243 | @media (max-width: 768px) { 244 | .logo-floo { 245 | font-size: 4rem; 246 | letter-spacing: 0.15em; 247 | } 248 | 249 | .hero-title { 250 | font-size: 3rem; 251 | } 252 | 253 | .metric-value { 254 | font-size: 2.5rem; 255 | } 256 | 257 | .hero-stats { 258 | grid-template-columns: repeat(2, 1fr); 259 | padding: 2rem; 260 | } 261 | 262 | .hero-subtitle { 263 | font-size: 1.25rem; 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /configs/floos.example.toml: -------------------------------------------------------------------------------- 1 | # ============================================================================ 2 | # Floo Server Configuration (floos.toml) 3 | # ============================================================================ 4 | # 5 | # This is the SERVER component that runs on your public VPS or relay server. 6 | # Clients (flooc) connect to this server to establish encrypted tunnels. 7 | # 8 | # Quick Start: 9 | # 1. Generate strong credentials: 10 | # openssl rand -base64 32 # For PSK 11 | # openssl rand -base64 24 # For token 12 | # 13 | # 2. Replace the placeholder values below with your generated credentials 14 | # 15 | # 3. Define your services (forward mode) or reverse_services (reverse mode) 16 | # 17 | # 4. Run: ./floos floos.toml 18 | # 19 | # ============================================================================ 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Core Server Settings (REQUIRED) 23 | # ---------------------------------------------------------------------------- 24 | 25 | bind = "0.0.0.0" # Listen on all interfaces (use "127.0.0.1" for local only) 26 | port = 8443 # Port for tunnel connections (clients connect here) 27 | 28 | # ---------------------------------------------------------------------------- 29 | # Security Settings (REQUIRED - REPLACE THESE!) 30 | # ---------------------------------------------------------------------------- 31 | 32 | # Cipher: Choose based on your CPU 33 | # - aes256gcm (recommended): Fast on modern CPUs with AES-NI 34 | # - aegis128l: Fastest on ARMv8+ and modern x86 (22+ Gbps) 35 | # - chacha20poly1305: Best for older CPUs without AES-NI 36 | # - aes128gcm: Faster than aes256gcm, still secure 37 | cipher = "aes256gcm" 38 | 39 | # PSK (Pre-Shared Key): MUST be strong and random! 40 | # Generate with: openssl rand -base64 32 41 | # ⚠️ NEVER use the placeholder below - the server will refuse to start! 42 | psk = "REPLACE_WITH_OPENSSL_RAND_BASE64_32_OUTPUT" 43 | 44 | # Default token: Used for service authentication 45 | # Generate with: openssl rand -base64 24 46 | # ⚠️ NEVER use the placeholder below - the server will refuse to start! 47 | token = "REPLACE_WITH_OPENSSL_RAND_BASE64_24_OUTPUT" 48 | 49 | # ---------------------------------------------------------------------------- 50 | # Forward Mode Services 51 | # ---------------------------------------------------------------------------- 52 | # Forward mode: Clients connect through the tunnel to reach these targets. 53 | # Format: service_name = "target_host:port[/transport]" 54 | # 55 | # Example use case: Allow remote access to internal/private networks 56 | # ---------------------------------------------------------------------------- 57 | 58 | [services] 59 | # Example: Expose internal API server 60 | # Clients will connect to this through the tunnel 61 | # api = "10.0.0.10:8080" 62 | 63 | # Example: PostgreSQL database in private subnet 64 | # database = "10.0.0.20:5432" 65 | # database.token = "unique-db-token-here" # Optional: per-service token 66 | 67 | # Example: Internal Kubernetes dashboard 68 | # k8s_dashboard = "192.168.1.100:8443" 69 | 70 | # Example: UDP service (DNS, VoIP, etc.) 71 | # dns = "10.0.0.53:53/udp" 72 | 73 | # ---------------------------------------------------------------------------- 74 | # Reverse Mode Services 75 | # ---------------------------------------------------------------------------- 76 | # Reverse mode: Accept connections from clients and expose them publicly 77 | # Format: service_name = "bind_host:port[/transport]" 78 | # 79 | # Example use case: Share home services without port forwarding/dynamic DNS 80 | # ---------------------------------------------------------------------------- 81 | 82 | [reverse_services] 83 | # Example: Accept media server connections from home 84 | # Friends can access http://your-vps-ip:8096 85 | # media = "0.0.0.0:8096" 86 | 87 | # Example: SSH access to home machines 88 | # ssh = "0.0.0.0:2222" 89 | # ssh.token = "unique-ssh-token-here" # Recommended for sensitive services 90 | 91 | # Example: Home security camera streams 92 | # camera = "0.0.0.0:5000/udp" 93 | 94 | # ---------------------------------------------------------------------------- 95 | # Advanced Performance Tuning (OPTIONAL) 96 | # ---------------------------------------------------------------------------- 97 | # Only modify these if you understand the implications 98 | # Default values work well for most deployments 99 | # ---------------------------------------------------------------------------- 100 | 101 | [advanced] 102 | # Socket buffer size in bytes (default: 8MB) 103 | # Increase for high-bandwidth connections (e.g., 16777216 for 16MB) 104 | socket_buffer_size = 8388608 105 | 106 | # UDP session timeout in seconds (default: 60) 107 | # How long to keep UDP "connections" alive without traffic 108 | udp_timeout_seconds = 60 109 | 110 | # Pin worker threads to dedicated CPU cores (Linux/Unix) 111 | pin_threads = true 112 | 113 | # Per-stream IO buffer size (in bytes) 114 | io_batch_bytes = 131072 115 | 116 | # TCP optimization settings 117 | tcp_nodelay = true # Disable Nagle's algorithm (lower latency) 118 | tcp_keepalive = true # Enable TCP keepalive 119 | tcp_keepalive_idle = 45 # Seconds before first keepalive probe 120 | tcp_keepalive_interval = 10 # Seconds between keepalive probes 121 | tcp_keepalive_count = 3 # Number of failed probes before timeout 122 | 123 | # Heartbeat settings (detect dead connections) 124 | heartbeat_interval_seconds = 30 # How often to send heartbeats 125 | heartbeat_timeout_seconds = 60 # Consider connection dead after this 126 | 127 | # ---------------------------------------------------------------------------- 128 | # Security Best Practices 129 | # ---------------------------------------------------------------------------- 130 | # 131 | # ✅ DO: 132 | # - Use strong random credentials (openssl rand -base64) 133 | # - Use different tokens for different services 134 | # - Rotate credentials periodically 135 | # - Use per-service tokens for sensitive services (database, ssh) 136 | # - Monitor connection logs 137 | # 138 | # ❌ DON'T: 139 | # - Use weak or default credentials (server will refuse to start) 140 | # - Commit credentials to version control 141 | # - Share credentials over unsecured channels 142 | # - Use cipher = "none" in production (debugging only!) 143 | # - Bind reverse_services to 0.0.0.0 unless you want public access 144 | # 145 | # ---------------------------------------------------------------------------- 146 | -------------------------------------------------------------------------------- /.github/workflows/publish-apt.yml: -------------------------------------------------------------------------------- 1 | name: Publish APT Repository 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: 'Version to publish (e.g., 0.1.2)' 10 | required: true 11 | 12 | jobs: 13 | build-deb: 14 | name: Build .deb Packages 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: Install dependencies 22 | run: | 23 | sudo apt-get update 24 | sudo apt-get install -y dpkg-dev 25 | 26 | - name: Determine version 27 | id: version 28 | run: | 29 | if [ "${{ github.event_name }}" = "release" ]; then 30 | VERSION="${GITHUB_REF#refs/tags/v}" 31 | else 32 | VERSION="${{ github.event.inputs.version }}" 33 | fi 34 | echo "version=${VERSION}" >> $GITHUB_OUTPUT 35 | echo "Building version: ${VERSION}" 36 | 37 | - name: Build .deb packages 38 | run: | 39 | cd packaging 40 | ./build-deb.sh ${{ steps.version.outputs.version }} 41 | 42 | - name: Upload .deb artifacts 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: deb-packages 46 | path: packaging/build-deb/*.deb 47 | 48 | publish-apt: 49 | name: Publish to APT Repository 50 | runs-on: ubuntu-latest 51 | needs: build-deb 52 | if: github.repository == 'YUX/floo' # Only run on main repo 53 | 54 | steps: 55 | - name: Checkout code 56 | uses: actions/checkout@v4 57 | 58 | - name: Download .deb packages 59 | uses: actions/download-artifact@v4 60 | with: 61 | name: deb-packages 62 | path: packaging/build-deb/ 63 | 64 | - name: Checkout APT repository 65 | uses: actions/checkout@v4 66 | with: 67 | repository: YUX/floo-apt # Change to your APT repo 68 | path: apt-repo 69 | token: ${{ secrets.APT_REPO_TOKEN }} # Personal access token with repo write 70 | 71 | - name: Determine version 72 | id: version 73 | run: | 74 | if [ "${{ github.event_name }}" = "release" ]; then 75 | VERSION="${GITHUB_REF#refs/tags/v}" 76 | else 77 | VERSION="${{ github.event.inputs.version }}" 78 | fi 79 | echo "version=${VERSION}" >> $GITHUB_OUTPUT 80 | 81 | - name: Install dependencies 82 | run: | 83 | sudo apt-get update 84 | sudo apt-get install -y dpkg-dev tree 85 | 86 | - name: Copy packages to repository 87 | run: | 88 | mkdir -p apt-repo/pool/main 89 | cp packaging/build-deb/*.deb apt-repo/pool/main/ 90 | 91 | - name: Generate repository metadata 92 | run: | 93 | cd apt-repo 94 | 95 | # Create directory structure 96 | mkdir -p dists/stable/main/binary-amd64 97 | mkdir -p dists/stable/main/binary-arm64 98 | 99 | # Generate Packages files 100 | dpkg-scanpackages --arch amd64 pool/ > dists/stable/main/binary-amd64/Packages 101 | gzip -k -f dists/stable/main/binary-amd64/Packages 102 | 103 | dpkg-scanpackages --arch arm64 pool/ > dists/stable/main/binary-arm64/Packages 104 | gzip -k -f dists/stable/main/binary-arm64/Packages 105 | 106 | # Generate Release file 107 | cat > dists/stable/Release << EOF 108 | Origin: Floo 109 | Label: Floo 110 | Suite: stable 111 | Codename: stable 112 | Version: 1.0 113 | Architectures: amd64 arm64 114 | Components: main 115 | Description: Floo APT Repository - High-performance tunneling in Zig 116 | Date: $(date -R) 117 | EOF 118 | 119 | # Add checksums 120 | echo "MD5Sum:" >> dists/stable/Release 121 | find dists/stable/main -type f -exec md5sum {} \; | sed 's|dists/stable/||' >> dists/stable/Release 122 | 123 | echo "SHA1:" >> dists/stable/Release 124 | find dists/stable/main -type f -exec sha1sum {} \; | sed 's|dists/stable/||' >> dists/stable/Release 125 | 126 | echo "SHA256:" >> dists/stable/Release 127 | find dists/stable/main -type f -exec sha256sum {} \; | sed 's|dists/stable/||' >> dists/stable/Release 128 | 129 | # Optional: Sign with GPG if GPG_PRIVATE_KEY secret is set 130 | - name: Import GPG key 131 | if: ${{ secrets.GPG_PRIVATE_KEY != '' }} 132 | run: | 133 | echo "${{ secrets.GPG_PRIVATE_KEY }}" | gpg --import 134 | GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format=long | grep sec | awk '{print $2}' | cut -d'/' -f2) 135 | echo "GPG_KEY_ID=${GPG_KEY_ID}" >> $GITHUB_ENV 136 | 137 | - name: Sign Release 138 | if: ${{ secrets.GPG_PRIVATE_KEY != '' }} 139 | run: | 140 | cd apt-repo/dists/stable 141 | gpg --default-key ${{ env.GPG_KEY_ID }} -abs -o Release.gpg Release 142 | gpg --default-key ${{ env.GPG_KEY_ID }} --clearsign -o InRelease Release 143 | cd ../../ 144 | gpg --armor --export ${{ env.GPG_KEY_ID }} > pubkey.gpg 145 | 146 | - name: Create README 147 | run: | 148 | cat > apt-repo/README.md << 'EOF' 149 | # Floo APT Repository 150 | 151 | Official APT repository for Floo - High-performance tunneling in Zig. 152 | 153 | ## Installation 154 | 155 | ### Add Repository 156 | 157 | ```bash 158 | # Add repository 159 | curl -fsSL https://yux.github.io/floo-apt/pubkey.gpg | sudo gpg --dearmor -o /usr/share/keyrings/floo.gpg 160 | echo 'deb [signed-by=/usr/share/keyrings/floo.gpg] https://yux.github.io/floo-apt stable main' | sudo tee /etc/apt/sources.list.d/floo.list 161 | 162 | # Update and install 163 | sudo apt update 164 | sudo apt install floo 165 | ``` 166 | 167 | ### Verify Installation 168 | 169 | ```bash 170 | flooc --version 171 | floos --version 172 | ``` 173 | 174 | ## Available Packages 175 | 176 | - `floo` - Both client (flooc) and server (floos) binaries 177 | 178 | ## Supported Architectures 179 | 180 | - amd64 (x86_64) 181 | - arm64 (aarch64) 182 | 183 | ## Documentation 184 | 185 | See [main repository](https://github.com/YUX/floo) for complete documentation. 186 | EOF 187 | 188 | - name: Commit and push 189 | run: | 190 | cd apt-repo 191 | git config user.name "GitHub Actions" 192 | git config user.email "actions@github.com" 193 | git add . 194 | git commit -m "Update packages for v${{ steps.version.outputs.version }}" || echo "No changes" 195 | git push 196 | -------------------------------------------------------------------------------- /src/net_compat.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const posix = std.posix; 4 | const native_endian = builtin.cpu.arch.endian(); 5 | 6 | pub const Address = extern union { 7 | any: posix.sockaddr, 8 | in: posix.sockaddr.in, 9 | in6: posix.sockaddr.in6, 10 | 11 | pub fn initIp4(address: [4]u8, port: u16) Address { 12 | return .{ 13 | .in = .{ 14 | .family = posix.AF.INET, 15 | .port = std.mem.nativeToBig(u16, port), 16 | .addr = std.mem.readInt(u32, &address, native_endian), 17 | .zero = std.mem.zeroes([8]u8), 18 | }, 19 | }; 20 | } 21 | 22 | pub fn initPosix(addr: *const posix.sockaddr) Address { 23 | switch (addr.family) { 24 | posix.AF.INET => return .{ .in = @as(*const posix.sockaddr.in, @ptrCast(@alignCast(addr))).* }, 25 | posix.AF.INET6 => return .{ .in6 = @as(*const posix.sockaddr.in6, @ptrCast(@alignCast(addr))).* }, 26 | else => return .{ .any = addr.* }, 27 | } 28 | } 29 | 30 | pub fn getPort(self: Address) u16 { 31 | return switch (self.any.family) { 32 | posix.AF.INET => std.mem.bigToNative(u16, self.in.port), 33 | posix.AF.INET6 => std.mem.bigToNative(u16, self.in6.port), 34 | else => 0, 35 | }; 36 | } 37 | 38 | pub fn parseIp4(host: []const u8, port: u16) !Address { 39 | var addr: u32 = 0; 40 | var octets: u32 = 0; 41 | var seen_octets: u3 = 0; 42 | 43 | for (host) |char| { 44 | if (char == '.') { 45 | if (octets > 255) return error.InvalidIp; 46 | addr = (addr << 8) | octets; 47 | octets = 0; 48 | seen_octets += 1; 49 | if (seen_octets > 3) return error.InvalidIp; 50 | } else if (std.ascii.isDigit(char)) { 51 | octets = octets * 10 + (char - '0'); 52 | if (octets > 255 and seen_octets < 3) return error.InvalidIp; // optimization 53 | } else { 54 | return error.InvalidIp; 55 | } 56 | } 57 | if (octets > 255) return error.InvalidIp; 58 | if (seen_octets != 3) return error.InvalidIp; 59 | addr = (addr << 8) | octets; 60 | 61 | // In sockaddr.in.addr, it expects network byte order (Big Endian) 62 | // Our manual parse produced host byte order (if we treat it as a u32) 63 | // Wait, actually: 1.2.3.4 -> 0x01020304. 64 | // std.mem.readInt reads bytes. 65 | // Let's just use the byte array logic which is safer. 66 | 67 | // Simpler way: 68 | var it = std.mem.splitScalar(u8, host, '.'); 69 | var bytes: [4]u8 = undefined; 70 | var index: usize = 0; 71 | while (it.next()) |part| { 72 | if (index >= 4) return error.InvalidIp; 73 | bytes[index] = std.fmt.parseInt(u8, part, 10) catch return error.InvalidIp; 74 | index += 1; 75 | } 76 | if (index != 4) return error.InvalidIp; 77 | 78 | return initIp4(bytes, port); 79 | } 80 | 81 | pub fn parseIp6(host: []const u8, port: u16) !Address { 82 | _ = host; 83 | _ = port; 84 | // TODO: Implement IPv6 parsing if needed. For now return error to fallback to resolveIp 85 | return error.InvalidIp; 86 | } 87 | 88 | pub fn resolveIp(host: []const u8, port: u16) !Address { 89 | const c = std.c; 90 | var hints: c.addrinfo = undefined; 91 | // hints must be zeroed? 92 | @memset(@as([*]u8, @ptrCast(&hints))[0..@sizeOf(c.addrinfo)], 0); 93 | 94 | hints.flags = std.mem.zeroes(@TypeOf(hints.flags)); 95 | hints.family = posix.AF.UNSPEC; 96 | hints.socktype = posix.SOCK.STREAM; 97 | hints.protocol = posix.IPPROTO.TCP; 98 | 99 | // Host string needs to be null-terminated for C 100 | var host_z_buf: [256]u8 = undefined; 101 | if (host.len >= host_z_buf.len) return error.NameTooLong; 102 | @memcpy(host_z_buf[0..host.len], host); 103 | host_z_buf[host.len] = 0; 104 | const host_z = host_z_buf[0..host.len :0]; 105 | 106 | var port_buf: [16]u8 = undefined; 107 | const port_str = std.fmt.bufPrintZ(&port_buf, "{}", .{port}) catch return error.Unexpected; 108 | 109 | var res: ?*c.addrinfo = null; 110 | const rc = c.getaddrinfo(host_z, port_str, &hints, &res); 111 | if (@intFromEnum(rc) != 0) { 112 | return error.UnknownHost; 113 | } 114 | defer if (res) |r| c.freeaddrinfo(r); 115 | 116 | if (res) |r| { 117 | if (r.addr) |addr_ptr| { 118 | if (r.family == posix.AF.INET) { 119 | const addr_in = @as(*const posix.sockaddr.in, @ptrCast(@alignCast(addr_ptr))); 120 | return .{ .in = addr_in.* }; 121 | } else if (r.family == posix.AF.INET6) { 122 | const addr_in6 = @as(*const posix.sockaddr.in6, @ptrCast(@alignCast(addr_ptr))); 123 | return .{ .in6 = addr_in6.* }; 124 | } 125 | } 126 | } 127 | return error.UnknownHost; 128 | } 129 | 130 | pub fn getOsSockLen(self: Address) posix.socklen_t { 131 | return switch (self.any.family) { 132 | posix.AF.INET => @sizeOf(posix.sockaddr.in), 133 | posix.AF.INET6 => @sizeOf(posix.sockaddr.in6), 134 | else => @sizeOf(posix.sockaddr), 135 | }; 136 | } 137 | 138 | pub fn format(self: Address, writer: anytype) !void { 139 | switch (self.any.family) { 140 | posix.AF.INET => { 141 | const addr = self.in.addr; // u32 big endian (network order) 142 | // We need to read it as bytes. 143 | // addr is u32. In net compat, we initialized it via initIp4 which put bytes in memory. 144 | // But posix.sockaddr.in.addr is u32. 145 | // Let's just cast pointer to *[4]u8. 146 | const bytes = @as(*const [4]u8, @ptrCast(&addr)); 147 | try writer.print("{}.{}.{}.{}:{}", .{ bytes[0], bytes[1], bytes[2], bytes[3], std.mem.bigToNative(u16, self.in.port) }); 148 | }, 149 | posix.AF.INET6 => { 150 | try writer.print("[IPv6]:{}", .{std.mem.bigToNative(u16, self.in6.port)}); 151 | }, 152 | else => try writer.writeAll("unknown-address"), 153 | } 154 | } 155 | }; 156 | 157 | pub fn getAddressList(allocator: std.mem.Allocator, host: []const u8, port: u16) !AddressList { 158 | // Mock implementation that returns a single address using resolveIp 159 | const addr = try Address.resolveIp(host, port); 160 | const list = try allocator.alloc(Address, 1); 161 | list[0] = addr; 162 | return AddressList{ .allocator = allocator, .addrs = list }; 163 | } 164 | 165 | pub const AddressList = struct { 166 | allocator: std.mem.Allocator, 167 | addrs: []Address, 168 | 169 | pub fn deinit(self: AddressList) void { 170 | self.allocator.free(self.addrs); 171 | } 172 | }; 173 | -------------------------------------------------------------------------------- /src/udp_client.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const posix = std.posix; 3 | const net = @import("net_compat.zig"); 4 | const tunnel = @import("tunnel.zig"); 5 | const noise = @import("noise.zig"); 6 | const udp_session = @import("udp_session.zig"); 7 | const common = @import("common.zig"); 8 | const resolveHostPort = common.resolveHostPort; 9 | 10 | /// UDP forwarder for client side 11 | /// Handles local UDP clients and forwards through tunnel 12 | /// 13 | /// Design: Client listens on local UDP port and tracks sessions for each 14 | /// local client that sends packets. Each unique source address gets a 15 | /// session ID that's used to route responses back correctly. 16 | pub const UdpForwarder = struct { 17 | allocator: std.mem.Allocator, 18 | service_id: tunnel.ServiceId, 19 | local_port: u16, 20 | local_fd: posix.fd_t, 21 | tunnel_conn: *anyopaque, // Opaque pointer to TunnelClient 22 | send_fn: *const fn (conn: *anyopaque, buffer: []u8, payload_len: usize) anyerror!void, 23 | running: std.atomic.Value(bool), 24 | thread: std.Thread, 25 | session_manager: udp_session.UdpSessionManager, 26 | timeout_seconds: u64, 27 | 28 | pub fn create( 29 | allocator: std.mem.Allocator, 30 | service_id: tunnel.ServiceId, 31 | local_host: []const u8, 32 | local_port: u16, 33 | tunnel_conn: *anyopaque, 34 | send_fn: *const fn (conn: *anyopaque, buffer: []u8, payload_len: usize) anyerror!void, 35 | timeout_seconds: u64, 36 | ) !*UdpForwarder { 37 | // Resolve and bind local UDP socket 38 | const bind_addr = try resolveHostPort(local_host, local_port); 39 | const local_fd = try posix.socket( 40 | bind_addr.any.family, 41 | posix.SOCK.DGRAM | posix.SOCK.CLOEXEC, 42 | 0, 43 | ); 44 | errdefer posix.close(local_fd); 45 | 46 | // Bind to local host/port 47 | try posix.bind(local_fd, &bind_addr.any, bind_addr.getOsSockLen()); 48 | 49 | const forwarder = try allocator.create(UdpForwarder); 50 | forwarder.* = .{ 51 | .allocator = allocator, 52 | .service_id = service_id, 53 | .local_port = local_port, 54 | .local_fd = local_fd, 55 | .tunnel_conn = tunnel_conn, 56 | .send_fn = send_fn, 57 | .running = std.atomic.Value(bool).init(true), 58 | .thread = undefined, 59 | .session_manager = udp_session.UdpSessionManager.init(allocator), 60 | .timeout_seconds = timeout_seconds, 61 | }; 62 | 63 | // Start local receiver thread 64 | forwarder.thread = try std.Thread.spawn(.{ 65 | .stack_size = common.DEFAULT_THREAD_STACK, 66 | }, localReceiveThread, .{forwarder}); 67 | 68 | std.debug.print("[UDP-CLIENT] Listening on {s}:{}\n", .{ local_host, local_port }); 69 | 70 | return forwarder; 71 | } 72 | 73 | fn localReceiveThread(self: *UdpForwarder) void { 74 | var buf: [common.SOCKET_BUFFER_SIZE]u8 align(64) = undefined; 75 | var from_addr: posix.sockaddr.storage = undefined; 76 | var from_len: posix.socklen_t = @sizeOf(posix.sockaddr.storage); 77 | 78 | std.debug.print("[UDP-CLIENT] Local receiver thread started\n", .{}); 79 | 80 | while (self.running.load(.acquire)) { 81 | // Receive UDP packet from local client 82 | const n = posix.recvfrom( 83 | self.local_fd, 84 | &buf, 85 | 0, 86 | @ptrCast(&from_addr), 87 | &from_len, 88 | ) catch |err| { 89 | std.debug.print("[UDP-CLIENT] recvfrom error: {}\n", .{err}); 90 | continue; 91 | }; 92 | 93 | if (n == 0) continue; 94 | 95 | // Convert source address 96 | const source_addr = net.Address.initPosix(@ptrCast(@alignCast(&from_addr))); 97 | 98 | // Get or create session for this local source 99 | const session = self.session_manager.getOrCreate(source_addr) catch |err| { 100 | std.debug.print("[UDP-CLIENT] Session creation error: {}\n", .{err}); 101 | continue; 102 | }; 103 | 104 | std.debug.print("[UDP-CLIENT] Received {} bytes from local {any}, stream_id={}\n", .{ 105 | n, 106 | source_addr, 107 | session.stream_id, 108 | }); 109 | 110 | // Encode UDP data message 111 | var encode_buf: [70016]u8 = undefined; 112 | 113 | // Get source address bytes for encoding 114 | var addr_bytes: [16]u8 = undefined; 115 | var addr_len: usize = 0; 116 | var source_port: u16 = 0; 117 | 118 | switch (source_addr.any.family) { 119 | posix.AF.INET => { 120 | const ipv4 = source_addr.in; 121 | @memcpy(addr_bytes[0..4], std.mem.asBytes(&ipv4.addr)); 122 | addr_len = 4; 123 | source_port = std.mem.bigToNative(u16, ipv4.port); 124 | }, 125 | posix.AF.INET6 => { 126 | const ipv6 = source_addr.in6; 127 | @memcpy(&addr_bytes, &ipv6.addr); 128 | addr_len = 16; 129 | source_port = std.mem.bigToNative(u16, ipv6.port); 130 | }, 131 | else => continue, 132 | } 133 | 134 | const udp_msg = tunnel.UdpDataMsg{ 135 | .service_id = self.service_id, 136 | .stream_id = session.stream_id, 137 | .source_addr = addr_bytes[0..addr_len], 138 | .source_port = source_port, 139 | .data = buf[0..n], 140 | }; 141 | 142 | const encoded_len = udp_msg.encodeInto(&encode_buf) catch |err| { 143 | std.debug.print("[UDP-CLIENT] Encode error: {}\n", .{err}); 144 | continue; 145 | }; 146 | 147 | // Send through tunnel 148 | self.send_fn(self.tunnel_conn, encode_buf[0 .. encoded_len + noise.TAG_LEN], encoded_len) catch |err| { 149 | std.debug.print("[UDP-CLIENT] Tunnel send error: {}\n", .{err}); 150 | }; 151 | } 152 | 153 | std.debug.print("[UDP-CLIENT] Local receiver thread stopped\n", .{}); 154 | } 155 | 156 | /// Handle incoming UDP data from tunnel (forward to local client) 157 | pub fn handleUdpData(self: *UdpForwarder, udp_msg: tunnel.UdpDataMsg) !void { 158 | // Look up session by stream_id 159 | const session = self.session_manager.getByStreamId(udp_msg.stream_id) orelse { 160 | std.debug.print("[UDP-CLIENT] Unknown stream_id={}, dropping packet\n", .{udp_msg.stream_id}); 161 | return; 162 | }; 163 | 164 | // Send back to local source address 165 | _ = try posix.sendto( 166 | self.local_fd, 167 | udp_msg.data, 168 | 0, 169 | &session.source_addr.any, 170 | session.source_addr.getOsSockLen(), 171 | ); 172 | 173 | std.debug.print("[UDP-CLIENT] Forwarded {} bytes to local {any}\n", .{ 174 | udp_msg.data.len, 175 | session.source_addr, 176 | }); 177 | } 178 | 179 | /// Cleanup expired sessions 180 | pub fn cleanupExpiredSessions(self: *UdpForwarder) !void { 181 | const removed = try self.session_manager.cleanupExpired(self.timeout_seconds); 182 | if (removed > 0) { 183 | std.debug.print("[UDP-CLIENT] Cleaned up {} expired sessions\n", .{removed}); 184 | } 185 | } 186 | 187 | pub fn stop(self: *UdpForwarder) void { 188 | self.running.store(false, .release); 189 | // Shutdown socket to unblock recvfrom() 190 | posix.shutdown(self.local_fd, .recv) catch {}; 191 | self.thread.join(); 192 | } 193 | 194 | pub fn destroy(self: *UdpForwarder) void { 195 | posix.close(self.local_fd); 196 | self.session_manager.deinit(); 197 | self.allocator.destroy(self); 198 | } 199 | }; 200 | -------------------------------------------------------------------------------- /examples/expose-home-server/README.md: -------------------------------------------------------------------------------- 1 | # Expose a Home Media Server (Jellyfin/Plex/Emby) 2 | 3 | Share your home media server with friends without opening router ports or dealing with dynamic DNS. Floo creates a secure encrypted tunnel from your home to a public VPS, making your media server accessible on the internet. 4 | 5 | ## 📋 What You Need 6 | 7 | - **Home machine** running Jellyfin/Plex/Emby 8 | - **Public VPS** (DigitalOcean, AWS, etc.) with a static IP 9 | - **5 minutes** to set up 10 | 11 | ## 🏗️ How It Works 12 | 13 | ``` 14 | ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ 15 | │ Friend's │ │ Your VPS │ │ Your Home │ 16 | │ Browser │──────────────│ (floos) │──────────────│ (flooc + │ 17 | │ │ Internet │ │ Encrypted │ Jellyfin) │ 18 | │ │ │ Listens :80 │ Tunnel │ 127.0.0.1 │ 19 | └─────────────────┘ └─────────────────┘ └──────────────┘ 20 | 21 | Friend visits Relay traffic Media server 22 | http://vps-ip/ over secure tunnel stays private 23 | ``` 24 | 25 | **Key benefit**: No port forwarding or dynamic DNS needed. Your home router stays secure with no exposed ports. 26 | 27 | --- 28 | 29 | ## 🚀 Step-by-Step Setup 30 | 31 | ### Step 1: Generate Strong Credentials 32 | 33 | On your VPS or home machine, generate random credentials: 34 | 35 | ```bash 36 | # Generate PSK (Pre-Shared Key) 37 | openssl rand -base64 32 38 | 39 | # Generate authentication token 40 | openssl rand -base64 24 41 | ``` 42 | 43 | **Save both outputs** - you'll need them for both server and client configs. 44 | 45 | ### Step 2: Configure the Server (VPS) 46 | 47 | On your VPS, create `floos.toml`: 48 | 49 | ```toml 50 | bind = "0.0.0.0" 51 | port = 8443 # Clients connect here 52 | cipher = "aes256gcm" 53 | psk = "PASTE_YOUR_PSK_HERE" # ← From step 1 54 | token = "PASTE_YOUR_TOKEN_HERE" # ← From step 1 55 | 56 | [reverse_services] 57 | jellyfin = "0.0.0.0:8096" # Public access on port 8096 58 | 59 | [advanced] 60 | tcp_nodelay = true 61 | tcp_keepalive = true 62 | socket_buffer_size = 8388608 # 8MB buffers for streaming 63 | pin_threads = true 64 | io_batch_bytes = 131072 65 | heartbeat_interval_seconds = 30 66 | ``` 67 | 68 | **Start the server**: 69 | ```bash 70 | ./floos floos.toml 71 | # [SERVER] Port: 8443 72 | # [SERVER] Waiting for tunnel connections... 73 | ``` 74 | 75 | ### Step 3: Configure the Client (Home) 76 | 77 | On your home machine (where Jellyfin runs), create `flooc.toml`: 78 | 79 | ```toml 80 | server = "YOUR_VPS_IP:8443" # ← Your VPS address 81 | cipher = "aes256gcm" 82 | psk = "PASTE_SAME_PSK_HERE" # ← Must match server! 83 | token = "PASTE_SAME_TOKEN_HERE" # ← Must match server! 84 | 85 | [reverse_services] 86 | jellyfin = "127.0.0.1:8096" # Your local Jellyfin port 87 | 88 | [advanced] 89 | num_tunnels = 0 # Auto-match CPU cores (set >0 to override) 90 | pin_threads = true 91 | io_batch_bytes = 131072 92 | reconnect_enabled = true # Auto-reconnect if connection drops 93 | socket_buffer_size = 8388608 # 8MB buffers for streaming 94 | ``` 95 | 96 | **Start the client**: 97 | ```bash 98 | ./flooc flooc.toml 99 | # [CLIENT] Connected to tunnel server 100 | # [CLIENT] Reverse service 'jellyfin' ready 101 | ``` 102 | 103 | ### Step 4: Test It 104 | 105 | Open a browser and go to: 106 | ``` 107 | http://YOUR_VPS_IP:8096 108 | ``` 109 | 110 | You should see your Jellyfin login page! 🎉 111 | 112 | The traffic flows: 113 | 1. Friend → VPS:8096 114 | 2. VPS → (encrypted tunnel) → Your home machine 115 | 3. Your home → Jellyfin on localhost:8096 116 | 4. Response → back through tunnel → Friend 117 | 118 | --- 119 | 120 | ## ✅ Verify Setup 121 | 122 | Test your configuration before going live: 123 | 124 | ```bash 125 | # On home machine 126 | ./flooc --doctor flooc.toml 127 | # ✓ Configuration valid 128 | # ✓ Connected to tunnel server 129 | # ✓ Handshake completed 130 | ``` 131 | 132 | --- 133 | 134 | ## 🔒 Security Hardening (Recommended) 135 | 136 | ### Add HTTPS 137 | 138 | Put nginx/Caddy on your VPS to add HTTPS: 139 | 140 | ```nginx 141 | # /etc/nginx/sites-available/media 142 | server { 143 | listen 443 ssl; 144 | server_name media.yourdomain.com; 145 | 146 | ssl_certificate /etc/letsencrypt/live/media.yourdomain.com/fullchain.pem; 147 | ssl_certificate_key /etc/letsencrypt/live/media.yourdomain.com/privkey.pem; 148 | 149 | location / { 150 | proxy_pass http://127.0.0.1:8096; 151 | proxy_set_header Host $host; 152 | proxy_set_header X-Real-IP $remote_addr; 153 | } 154 | } 155 | ``` 156 | 157 | Then change floos.toml to bind on localhost: 158 | ```toml 159 | [reverse_services] 160 | jellyfin = "127.0.0.1:8096" # Only nginx can access 161 | ``` 162 | 163 | ### Run as System Service 164 | 165 | Create `/etc/systemd/system/flooc.service` on home machine: 166 | 167 | ```ini 168 | [Unit] 169 | Description=Floo Tunnel Client 170 | After=network.target 171 | 172 | [Service] 173 | Type=simple 174 | User=yourusername 175 | WorkingDirectory=/home/yourusername/floo 176 | ExecStart=/home/yourusername/floo/flooc flooc.toml 177 | Restart=always 178 | RestartSec=10 179 | 180 | [Install] 181 | WantedBy=multi-user.target 182 | ``` 183 | 184 | Enable and start: 185 | ```bash 186 | sudo systemctl enable flooc 187 | sudo systemctl start flooc 188 | ``` 189 | 190 | --- 191 | 192 | ## 🔧 Troubleshooting 193 | 194 | | Problem | Solution | 195 | |---------|----------| 196 | | **Connection refused** | Check VPS firewall allows port 8443 (TCP) | 197 | | **Handshake failed** | Verify PSK and token match exactly on both sides | 198 | | **Can't access Jellyfin** | Ensure Jellyfin runs on 127.0.0.1:8096 locally | 199 | | **Tunnel disconnects** | `reconnect_enabled = true` in flooc.toml (already set) | 200 | | **Slow streaming** | Leave `num_tunnels = 0` (auto) and raise `socket_buffer_size` / `io_batch_bytes` for bigger bursts | 201 | | **Server refuses to start** | Replace placeholder credentials with real ones! | 202 | 203 | ### Debug Commands 204 | 205 | ```bash 206 | # Check if Jellyfin is running locally 207 | curl http://127.0.0.1:8096 208 | 209 | # Check if VPS port is open 210 | nc -zv YOUR_VPS_IP 8443 211 | 212 | # View flooc logs 213 | ./flooc flooc.toml # Watch output for errors 214 | ``` 215 | 216 | --- 217 | 218 | ## 🎯 Advanced: Multiple Services 219 | 220 | Want to expose Jellyfin AND Plex? Easy: 221 | 222 | **floos.toml** (VPS): 223 | ```toml 224 | [reverse_services] 225 | jellyfin = "0.0.0.0:8096" 226 | plex = "0.0.0.0:32400" 227 | ``` 228 | 229 | **flooc.toml** (Home): 230 | ```toml 231 | [reverse_services] 232 | jellyfin = "127.0.0.1:8096" 233 | plex = "127.0.0.1:32400" 234 | ``` 235 | 236 | Each service can have its own token: 237 | ```toml 238 | [reverse_services] 239 | jellyfin = "0.0.0.0:8096" 240 | jellyfin.token = "friends-only-token" 241 | 242 | admin_panel = "0.0.0.0:9000" 243 | admin_panel.token = "admin-secret-token" 244 | ``` 245 | 246 | --- 247 | 248 | ## 💡 Performance Tips 249 | 250 | For 4K streaming: 251 | ```toml 252 | [advanced] 253 | socket_buffer_size = 8388608 # 8MB buffers 254 | num_tunnels = 0 # Auto-match CPU cores (set >0 to force) 255 | pin_threads = true # Keep tunnels on dedicated cores 256 | io_batch_bytes = 131072 # Larger per-stream batch 257 | tcp_nodelay = true # Lower latency 258 | ``` 259 | 260 | > 💡 Run `kill -USR1 $(pgrep flooc)` or `kill -USR1 $(pgrep floos)` to dump live 261 | > throughput and encryption timing stats while you tune these settings. 262 | 263 | For bandwidth monitoring, check your VPS: 264 | ```bash 265 | iftop -i eth0 # Monitor traffic in real-time 266 | ``` 267 | 268 | --- 269 | 270 | ## 📱 Mobile Access 271 | 272 | Your friends can access your server from their phones too! Just give them: 273 | ``` 274 | http://YOUR_VPS_IP:8096 275 | ``` 276 | 277 | Or better yet, set up a domain name: 278 | ``` 279 | http://media.yourdomain.com 280 | ``` 281 | 282 | --- 283 | 284 | **Questions?** Check the [main README](../../README.md) or open an issue. 285 | 286 | **Enjoying Floo?** ⭐ Star the repo and share with friends! 287 | -------------------------------------------------------------------------------- /configs/flooc.example.toml: -------------------------------------------------------------------------------- 1 | # ============================================================================ 2 | # Floo Client Configuration (flooc.toml) 3 | # ============================================================================ 4 | # 5 | # This is the CLIENT component that connects to a floo server (floos). 6 | # Use this to access remote services or expose local services. 7 | # 8 | # Quick Start: 9 | # 1. Get credentials from your server administrator (PSK and token) 10 | # OR generate them yourself: 11 | # openssl rand -base64 32 # For PSK 12 | # openssl rand -base64 24 # For token 13 | # 14 | # 2. Replace the placeholder values below with actual credentials 15 | # 16 | # 3. Define your services or reverse_services 17 | # 18 | # 4. Run: ./flooc flooc.toml 19 | # 20 | # ============================================================================ 21 | 22 | # ---------------------------------------------------------------------------- 23 | # Core Tunnel Settings (REQUIRED) 24 | # ---------------------------------------------------------------------------- 25 | 26 | # Server address (hostname or IP + port) 27 | # This is where your floo server (floos) is running 28 | server = "your-vps-hostname.com:8443" 29 | 30 | # ---------------------------------------------------------------------------- 31 | # Security Settings (REQUIRED - MUST MATCH SERVER!) 32 | # ---------------------------------------------------------------------------- 33 | 34 | # Cipher: MUST match the server's cipher setting 35 | # - aes256gcm (recommended): Fast on modern CPUs with AES-NI 36 | # - aegis128l: Fastest on ARMv8+ and modern x86 (22+ Gbps) 37 | # - chacha20poly1305: Best for older CPUs without AES-NI 38 | cipher = "aes256gcm" 39 | 40 | # PSK (Pre-Shared Key): MUST match server exactly! 41 | # Get this from your server administrator 42 | # ⚠️ NEVER use the placeholder below - the client will fail to connect! 43 | psk = "REPLACE_WITH_SERVER_PSK_HERE" 44 | 45 | # Default token: MUST match server's default token 46 | # Get this from your server administrator 47 | # ⚠️ NEVER use the placeholder below - connections will be rejected! 48 | token = "REPLACE_WITH_SERVER_TOKEN_HERE" 49 | 50 | # Optional: Auto-select this service when running in single-service mode 51 | # default_service = "web" 52 | 53 | # ---------------------------------------------------------------------------- 54 | # Forward Mode Services 55 | # ---------------------------------------------------------------------------- 56 | # Forward mode: Create local listeners that connect through the tunnel 57 | # Format: service_name = "listen_host:port[/transport]" 58 | # 59 | # Example: Access remote database as if it's running locally 60 | # ---------------------------------------------------------------------------- 61 | 62 | [services] 63 | # Example: Access remote web service via http://localhost:8080 64 | # web = "127.0.0.1:8080" 65 | 66 | # Example: SSH to remote machine via localhost:2222 67 | # ssh = "127.0.0.1:2222" 68 | # ssh.token = "specific-ssh-token" # If server requires different token 69 | 70 | # Example: Access remote PostgreSQL database 71 | # database = "127.0.0.1:5432" 72 | # database.token = "database-specific-token" 73 | 74 | # Example: Local DNS forwarding (UDP) 75 | # dns = "127.0.0.1:5300/udp" 76 | 77 | # ---------------------------------------------------------------------------- 78 | # Reverse Mode Services 79 | # ---------------------------------------------------------------------------- 80 | # Reverse mode: Expose local services through the server 81 | # Format: service_name = "local_host:port[/transport]" 82 | # 83 | # Example: Share your local Jellyfin server with friends 84 | # ---------------------------------------------------------------------------- 85 | 86 | [reverse_services] 87 | # Example: Expose local Jellyfin/Emby/Plex media server 88 | # Server binds 0.0.0.0:8096, forwards to your local 127.0.0.1:8096 89 | # media = "127.0.0.1:8096" 90 | 91 | # Example: Expose local SSH for remote access 92 | # ssh_home = "127.0.0.1:22" 93 | # ssh_home.token = "home-ssh-token" # Recommended for security 94 | 95 | # Example: Expose Prometheus metrics 96 | # metrics = "127.0.0.1:9090" 97 | 98 | # Example: UDP service (camera stream, VoIP, etc.) 99 | # camera = "127.0.0.1:5000/udp" 100 | 101 | # ---------------------------------------------------------------------------- 102 | # Advanced Tuning (OPTIONAL) 103 | # ---------------------------------------------------------------------------- 104 | # Most users don't need to change these - defaults work well 105 | # ---------------------------------------------------------------------------- 106 | 107 | [advanced] 108 | # Parallel tunnels (0 = auto, matches CPU cores) 109 | # Set >0 only when you need to force a specific number. 110 | num_tunnels = 0 111 | 112 | # Pin each tunnel handler to a dedicated CPU core (Linux/Unix only) 113 | pin_threads = true 114 | 115 | # Per-stream IO buffer size in bytes 116 | # Increase for high-latency links or very large frames. 117 | io_batch_bytes = 131072 118 | 119 | # UDP session timeout in seconds (default: 60) 120 | # How long to keep UDP "connections" alive without traffic 121 | udp_timeout_seconds = 60 122 | 123 | # Socket buffer size in bytes (default: 8MB) 124 | # Increase for high-bandwidth applications (e.g., 16777216 for 16MB) 125 | socket_buffer_size = 8388608 126 | 127 | # TCP optimization 128 | tcp_nodelay = true # Lower latency (disable Nagle) 129 | tcp_keepalive = true # Detect dead connections 130 | tcp_keepalive_idle = 45 # Seconds before first keepalive probe 131 | tcp_keepalive_interval = 10 # Seconds between keepalive probes 132 | tcp_keepalive_count = 3 # Failed probes before timeout 133 | 134 | # Heartbeat and reconnection 135 | heartbeat_timeout_seconds = 60 # Detect server disconnection 136 | reconnect_enabled = true # Auto-reconnect on disconnect 137 | reconnect_initial_delay_ms = 1000 # Wait 1 second after disconnect 138 | reconnect_max_delay_ms = 30000 # Max 30 seconds between attempts 139 | reconnect_backoff_multiplier = 2 # Exponential backoff 140 | 141 | # Proxy support (for corporate firewalls) 142 | # Supports: socks5://host:port or http://host:port 143 | # Leave empty if not using a proxy 144 | proxy_url = "" 145 | 146 | # Example proxy configurations: 147 | # proxy_url = "socks5://corporate-proxy:1080" 148 | # proxy_url = "http://corporate-proxy:8080" 149 | 150 | # ---------------------------------------------------------------------------- 151 | # Common Scenarios 152 | # ---------------------------------------------------------------------------- 153 | # 154 | # SCENARIO 1: Access home database from anywhere 155 | # ----------------------------------------------- 156 | # Server (floos.toml): 157 | # [services] 158 | # postgres = "10.0.0.5:5432" 159 | # 160 | # Client (flooc.toml): 161 | # [services] 162 | # postgres = "127.0.0.1:5432" 163 | # 164 | # Usage: psql -h 127.0.0.1 -p 5432 165 | # 166 | # SCENARIO 2: Share home media server 167 | # ------------------------------------ 168 | # Server (floos.toml): 169 | # [reverse_services] 170 | # jellyfin = "0.0.0.0:8096" 171 | # 172 | # Client (flooc.toml): 173 | # [reverse_services] 174 | # jellyfin = "127.0.0.1:8096" 175 | # 176 | # Friends access: http://your-vps-ip:8096 177 | # 178 | # SCENARIO 3: Through corporate firewall 179 | # --------------------------------------- 180 | # [advanced] 181 | # proxy_url = "socks5://corporate-proxy:1080" 182 | # 183 | # ---------------------------------------------------------------------------- 184 | # Security Best Practices 185 | # ---------------------------------------------------------------------------- 186 | # 187 | # ✅ DO: 188 | # - Keep credentials secure (never commit to git) 189 | # - Use different tokens for different services 190 | # - Verify server certificate/identity before connecting 191 | # - Monitor connection logs for suspicious activity 192 | # - Rotate credentials periodically 193 | # 194 | # ❌ DON'T: 195 | # - Use weak or default credentials (connection will fail) 196 | # - Share credentials over unsecured channels 197 | # - Connect to untrusted servers 198 | # - Use cipher = "none" on untrusted networks 199 | # 200 | # ---------------------------------------------------------------------------- 201 | -------------------------------------------------------------------------------- /src/udp_session.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const posix = std.posix; 3 | const net = @import("net_compat.zig"); 4 | const tunnel = @import("tunnel.zig"); 5 | const common = @import("common.zig"); 6 | 7 | /// UDP session key - identifies a unique UDP "connection" by source address 8 | /// We store the raw sockaddr to avoid issues with Address union 9 | pub const SessionKey = struct { 10 | family: u8, // AF.INET or AF.INET6 11 | addr_bytes: [16]u8, // IPv4 uses first 4 bytes, IPv6 uses all 16 12 | port: u16, // Network byte order 13 | 14 | pub fn initFromAddress(addr: net.Address) SessionKey { 15 | var key: SessionKey = undefined; 16 | key.family = @intCast(addr.any.family); 17 | key.addr_bytes = [_]u8{0} ** 16; 18 | 19 | switch (addr.any.family) { 20 | posix.AF.INET => { 21 | const ipv4 = addr.in; 22 | @memcpy(key.addr_bytes[0..4], std.mem.asBytes(&ipv4.addr)); 23 | key.port = ipv4.port; 24 | }, 25 | posix.AF.INET6 => { 26 | const ipv6 = addr.in6; 27 | @memcpy(&key.addr_bytes, &ipv6.addr); 28 | key.port = ipv6.port; 29 | }, 30 | else => unreachable, 31 | } 32 | 33 | return key; 34 | } 35 | 36 | pub fn eql(self: SessionKey, other: SessionKey) bool { 37 | return self.family == other.family and 38 | self.port == other.port and 39 | std.mem.eql(u8, &self.addr_bytes, &other.addr_bytes); 40 | } 41 | 42 | pub fn hash(self: SessionKey) u64 { 43 | var hasher = std.hash.Wyhash.init(0); 44 | hasher.update(&[_]u8{self.family}); 45 | hasher.update(&self.addr_bytes); 46 | hasher.update(std.mem.asBytes(&self.port)); 47 | return hasher.final(); 48 | } 49 | }; 50 | 51 | /// UDP session context 52 | pub const UdpSession = struct { 53 | stream_id: tunnel.StreamId, 54 | source_addr: net.Address, 55 | last_activity_ns: i128, // Nanoseconds since epoch 56 | 57 | pub fn init(stream_id: tunnel.StreamId, source_addr: net.Address) UdpSession { 58 | return .{ 59 | .stream_id = stream_id, 60 | .source_addr = source_addr, 61 | .last_activity_ns = common.nanoTimestamp(), 62 | }; 63 | } 64 | 65 | pub fn touch(self: *UdpSession) void { 66 | self.last_activity_ns = common.nanoTimestamp(); 67 | } 68 | 69 | pub fn isExpired(self: *const UdpSession, timeout_seconds: u64) bool { 70 | const now = common.nanoTimestamp(); 71 | const timeout_ns = @as(i128, timeout_seconds) * std.time.ns_per_s; 72 | return (now - self.last_activity_ns) > timeout_ns; 73 | } 74 | }; 75 | 76 | /// Context for managing UDP sessions 77 | pub const UdpSessionManager = struct { 78 | allocator: std.mem.Allocator, 79 | // Map: SessionKey -> UdpSession 80 | sessions: std.AutoHashMap(SessionKey, UdpSession), 81 | // Reverse map: stream_id -> SessionKey (for tunnel -> local forwarding) 82 | reverse_map: std.AutoHashMap(tunnel.StreamId, SessionKey), 83 | mutex: std.Thread.Mutex, 84 | next_stream_id: std.atomic.Value(u32), 85 | scratch_keys: std.ArrayListUnmanaged(SessionKey), 86 | 87 | pub fn init(allocator: std.mem.Allocator) UdpSessionManager { 88 | return .{ 89 | .allocator = allocator, 90 | .sessions = std.AutoHashMap(SessionKey, UdpSession).init(allocator), 91 | .reverse_map = std.AutoHashMap(tunnel.StreamId, SessionKey).init(allocator), 92 | .mutex = std.Thread.Mutex{}, 93 | .next_stream_id = std.atomic.Value(u32).init(1), 94 | .scratch_keys = .{}, 95 | }; 96 | } 97 | 98 | pub fn deinit(self: *UdpSessionManager) void { 99 | self.sessions.deinit(); 100 | self.reverse_map.deinit(); 101 | self.scratch_keys.deinit(self.allocator); 102 | } 103 | 104 | /// Get or create session for a source address 105 | pub fn getOrCreate(self: *UdpSessionManager, source_addr: net.Address) !UdpSession { 106 | self.mutex.lock(); 107 | defer self.mutex.unlock(); 108 | 109 | const key = SessionKey.initFromAddress(source_addr); 110 | 111 | if (self.sessions.get(key)) |*session| { 112 | // Existing session - update activity time 113 | var updated = session.*; 114 | updated.touch(); 115 | try self.sessions.put(key, updated); 116 | return updated; 117 | } 118 | 119 | // New session - allocate stream ID 120 | const stream_id = self.next_stream_id.fetchAdd(1, .monotonic); 121 | const session = UdpSession.init(stream_id, source_addr); 122 | 123 | try self.sessions.put(key, session); 124 | try self.reverse_map.put(stream_id, key); 125 | 126 | return session; 127 | } 128 | 129 | /// Look up session by stream_id (for reverse lookup) 130 | pub fn getByStreamId(self: *UdpSessionManager, stream_id: tunnel.StreamId) ?UdpSession { 131 | self.mutex.lock(); 132 | defer self.mutex.unlock(); 133 | 134 | const key = self.reverse_map.get(stream_id) orelse return null; 135 | return self.sessions.get(key); 136 | } 137 | 138 | /// Remove expired sessions 139 | pub fn cleanupExpired(self: *UdpSessionManager, timeout_seconds: u64) !usize { 140 | self.mutex.lock(); 141 | defer self.mutex.unlock(); 142 | 143 | self.scratch_keys.clearRetainingCapacity(); 144 | 145 | var iter = self.sessions.iterator(); 146 | while (iter.next()) |entry| { 147 | if (entry.value_ptr.isExpired(timeout_seconds)) { 148 | try self.scratch_keys.append(self.allocator, entry.key_ptr.*); 149 | } 150 | } 151 | 152 | // Remove expired sessions 153 | for (self.scratch_keys.items) |key| { 154 | if (self.sessions.fetchRemove(key)) |removed| { 155 | _ = self.reverse_map.remove(removed.value.stream_id); 156 | } 157 | } 158 | 159 | return self.scratch_keys.items.len; 160 | } 161 | 162 | /// Count active sessions 163 | pub fn count(self: *UdpSessionManager) usize { 164 | self.mutex.lock(); 165 | defer self.mutex.unlock(); 166 | return self.sessions.count(); 167 | } 168 | }; 169 | 170 | // Tests 171 | test "SessionKey equality and hashing" { 172 | const addr1 = try net.Address.parseIp4("127.0.0.1", 8080); 173 | const addr2 = try net.Address.parseIp4("127.0.0.1", 8080); 174 | const addr3 = try net.Address.parseIp4("127.0.0.1", 8081); 175 | 176 | const key1 = SessionKey.initFromAddress(addr1); 177 | const key2 = SessionKey.initFromAddress(addr2); 178 | const key3 = SessionKey.initFromAddress(addr3); 179 | 180 | try std.testing.expect(key1.eql(key2)); 181 | try std.testing.expect(!key1.eql(key3)); 182 | try std.testing.expectEqual(key1.hash(), key2.hash()); 183 | } 184 | 185 | test "UdpSession expiration" { 186 | const addr = try net.Address.parseIp4("127.0.0.1", 8080); 187 | var session = UdpSession.init(123, addr); 188 | 189 | // Fresh session should not be expired 190 | try std.testing.expect(!session.isExpired(60)); 191 | 192 | // Simulate old session by setting past timestamp 193 | session.last_activity_ns = common.nanoTimestamp() - (61 * std.time.ns_per_s); 194 | 195 | // Should now be expired with 60 second timeout 196 | try std.testing.expect(session.isExpired(60)); 197 | } 198 | 199 | test "UdpSessionManager basic operations" { 200 | const allocator = std.testing.allocator; 201 | var manager = UdpSessionManager.init(allocator); 202 | defer manager.deinit(); 203 | 204 | const addr1 = try net.Address.parseIp4("192.168.1.1", 12345); 205 | const addr2 = try net.Address.parseIp4("192.168.1.2", 12346); 206 | 207 | // Create first session 208 | const session1 = try manager.getOrCreate(addr1); 209 | try std.testing.expectEqual(@as(u32, 1), session1.stream_id); 210 | 211 | // Create second session 212 | const session2 = try manager.getOrCreate(addr2); 213 | try std.testing.expectEqual(@as(u32, 2), session2.stream_id); 214 | 215 | // Get existing session (should return same stream_id) 216 | const session1_again = try manager.getOrCreate(addr1); 217 | try std.testing.expectEqual(session1.stream_id, session1_again.stream_id); 218 | 219 | // Reverse lookup 220 | const found = manager.getByStreamId(session1.stream_id); 221 | try std.testing.expect(found != null); 222 | try std.testing.expectEqual(session1.stream_id, found.?.stream_id); 223 | 224 | // Count 225 | try std.testing.expectEqual(@as(usize, 2), manager.count()); 226 | } 227 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | tags: 7 | - 'v*' 8 | pull_request: 9 | branches: [main] 10 | workflow_dispatch: 11 | 12 | jobs: 13 | test: 14 | name: Run Tests 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup Zig 21 | uses: goto-bus-stop/setup-zig@v2 22 | with: 23 | version: master 24 | 25 | - name: Run tests 26 | run: zig build test 27 | 28 | - name: Build and test native 29 | run: | 30 | zig build -Doptimize=ReleaseFast 31 | ./zig-out/bin/flooc --version 32 | ./zig-out/bin/floos --version 33 | 34 | cross-compile: 35 | name: Cross-Compile Release Binaries 36 | runs-on: ubuntu-latest 37 | needs: test 38 | if: github.event_name == 'push' || startsWith(github.ref, 'refs/tags/v') 39 | 40 | steps: 41 | - name: Checkout code 42 | uses: actions/checkout@v4 43 | 44 | - name: Setup Zig 45 | uses: goto-bus-stop/setup-zig@v2 46 | with: 47 | version: master 48 | 49 | - name: Build all platforms 50 | run: | 51 | zig build release-all 52 | 53 | echo "Built binaries:" 54 | find zig-out/release -maxdepth 2 -type f \( -name "flooc*" -o -name "floos*" \) | sort 55 | 56 | - name: Package artifacts 57 | env: 58 | RELEASE_TARGETS: | 59 | x86_64-linux-gnu 60 | x86_64-linux-gnu-haswell 61 | x86_64-linux-musl 62 | aarch64-linux-gnu 63 | aarch64-linux-gnu-neoverse-n1 64 | aarch64-linux-gnu-rpi4 65 | x86_64-macos 66 | x86_64-macos-haswell 67 | aarch64-macos-m1 68 | run: | 69 | set -euo pipefail 70 | mkdir -p artifacts 71 | 72 | for target in $RELEASE_TARGETS; do 73 | src="zig-out/release/${target}" 74 | if [ ! -d "$src" ]; then 75 | echo "Warning: target ${target} missing, skipping" 76 | continue 77 | fi 78 | 79 | out_dir="artifacts/${target}" 80 | mkdir -p "$out_dir" 81 | 82 | for bin in flooc floos; do 83 | if [ -f "$src/${bin}.exe" ]; then 84 | cp "$src/${bin}.exe" "$out_dir/${bin}.exe" 85 | elif [ -f "$src/${bin}" ]; then 86 | cp "$src/${bin}" "$out_dir/${bin}" 87 | else 88 | echo "Warning: $bin not found for ${target}" 89 | fi 90 | done 91 | 92 | cp README.md "$out_dir/" 93 | cp configs/flooc.example.toml "$out_dir/flooc.toml.example" 94 | cp configs/floos.example.toml "$out_dir/floos.toml.example" 95 | cp CHANGELOG.md "$out_dir/" 2>/dev/null || true 96 | 97 | if [[ "$target" == *"windows"* ]]; then 98 | (cd artifacts && zip -rq "floo-${target}.zip" "${target}") 99 | else 100 | (cd artifacts && tar czf "floo-${target}.tar.gz" "${target}") 101 | fi 102 | done 103 | 104 | echo "Created release payloads:" 105 | ls -lh artifacts 106 | 107 | - name: Upload release packages 108 | uses: actions/upload-artifact@v4 109 | with: 110 | name: release-packages 111 | path: artifacts 112 | 113 | release: 114 | name: Create GitHub Release 115 | runs-on: ubuntu-latest 116 | needs: cross-compile 117 | if: startsWith(github.ref, 'refs/tags/v') 118 | permissions: 119 | contents: write 120 | 121 | steps: 122 | - name: Checkout code 123 | uses: actions/checkout@v4 124 | 125 | - name: Verify version matches tag 126 | run: | 127 | # Extract version from build.zig.zon 128 | VERSION=$(grep -oP '\.version = "\K[^"]+' build.zig.zon) 129 | # Extract version from git tag (remove 'v' prefix) 130 | TAG_VERSION=${GITHUB_REF#refs/tags/v} 131 | 132 | echo "build.zig.zon version: $VERSION" 133 | echo "Git tag version: $TAG_VERSION" 134 | 135 | if [ "$VERSION" != "$TAG_VERSION" ]; then 136 | echo "❌ ERROR: Version mismatch!" 137 | echo " build.zig.zon has version '$VERSION'" 138 | echo " Git tag is 'v$TAG_VERSION'" 139 | echo "" 140 | echo "Please update build.zig.zon to version \"$TAG_VERSION\" before creating this release." 141 | exit 1 142 | fi 143 | 144 | echo "✅ Version match confirmed: $VERSION" 145 | 146 | - name: Download all artifacts 147 | uses: actions/download-artifact@v4 148 | with: 149 | path: release-artifacts 150 | 151 | - name: Prepare release assets 152 | run: | 153 | mkdir -p release-assets 154 | find release-artifacts -type f \( -name "*.tar.gz" -o -name "*.zip" \) -exec cp {} release-assets/ \; 155 | ls -lh release-assets/ 156 | 157 | - name: Create Release 158 | uses: softprops/action-gh-release@v1 159 | with: 160 | files: release-assets/*.tar.gz 161 | draft: false 162 | prerelease: false 163 | generate_release_notes: true 164 | body: | 165 | ## Floo ${{ github.ref_name }} 166 | 167 | **High-throughput tunneling in Zig - Zero dependencies, maximum performance** 168 | 169 | ### 🚀 Performance (Apple M1 MacBook Air) 170 | - **29.4 Gbps** encrypted throughput (AEGIS-128L cipher) 171 | - **62% faster** than Rathole 172 | - **194% faster** than FRP 173 | 174 | ### 📦 Binary Sizes 175 | - Client (`flooc`): 394 KB 176 | - Server (`floos`): 277 KB 177 | - **Total: 671 KB** 178 | 179 | ### ✨ Key Features 180 | - 🔐 Noise XX + PSK authentication (5 AEAD ciphers) 181 | - 🚀 Multi-service multiplexing 182 | - ⚡ Parallel tunnels with round-robin load balancing 183 | - 🌐 Proxy support (SOCKS5 + HTTP CONNECT) 184 | - 💓 Heartbeat supervision & auto-reconnect 185 | - 🔄 Hot config reload (SIGHUP) 186 | - 📊 Built-in diagnostics (`--doctor`, `--ping`) 187 | - 🎯 Per-service token authentication 188 | 189 | ### 📥 Installation 190 | 191 | **Choose your platform:** 192 | 193 | | Platform | Download | CPU Optimization | 194 | |----------|----------|------------------| 195 | | **Linux x86_64 (glibc)** | `floo-x86_64-linux-gnu.tar.gz` | Generic compatibility | 196 | | **Linux x86_64 (Haswell+)** ⚡ | `floo-x86_64-linux-gnu-haswell.tar.gz` | AES-NI/BMI2 tuned | 197 | | **Linux x86_64 (static)** | `floo-x86_64-linux-musl.tar.gz` | Works on Alpine / scratch containers | 198 | | **Linux ARM64 (generic)** | `floo-aarch64-linux-gnu.tar.gz` | ARM servers, Jetson | 199 | | **Linux ARM64 (Neoverse)** ⚡ | `floo-aarch64-linux-gnu-neoverse-n1.tar.gz` | Graviton / Ampere instances | 200 | | **Linux ARM64 (Raspberry Pi 4/5)** 🥧 | `floo-aarch64-linux-gnu-rpi4.tar.gz` | Tuned for Cortex-A72 | 201 | | **macOS Intel** | `floo-x86_64-macos.tar.gz` | macOS 11+ universal baseline | 202 | | **macOS Intel (Haswell+)** ⚡ | `floo-x86_64-macos-haswell.tar.gz` | Best perf on 2013+ Macs | 203 | | **macOS Apple Silicon** | `floo-aarch64-macos-m1.tar.gz` | M1/M2/M3/M4 with ARMv8 crypto | 204 | 205 | ⚡ **Recommended:** Optimized builds provide significantly better crypto performance (3-5x faster encryption). 206 | 207 | 🥧 **Raspberry Pi users:** Use the `aarch64-linux-gnu-rpi` variant to avoid illegal instruction errors on Pi 4/5. 208 | 209 | **Extract and use:** 210 | ```bash 211 | tar xzf floo-*.tar.gz # or unzip floo-*.zip on Windows 212 | cd floo-*/ 213 | ./flooc --version 214 | ./floos --version 215 | ``` 216 | 217 | ### 📖 Quick Start 218 | 219 | See the [README](https://github.com/${{ github.repository }}) for complete documentation including: 220 | - Configuration guide 221 | - CLI reference 222 | - Performance tuning 223 | - Proxy setup 224 | - Troubleshooting 225 | 226 | ### ⚠️ Security Note 227 | 228 | Before production use: 229 | 1. Replace placeholder PSK and tokens in config files 230 | 2. Run `--doctor` diagnostics to validate setup 231 | 3. Use AEGIS-128L or AES-256-GCM cipher (never "none") 232 | 233 | --- 234 | 235 | **Built with ❤️ in Zig** 236 | -------------------------------------------------------------------------------- /packaging/README.md: -------------------------------------------------------------------------------- 1 | # Floo Package Manager Configurations 2 | 3 | This directory contains package manager configurations for distributing Floo. 4 | 5 | ## Homebrew (macOS) 6 | 7 | ### Option 1: Homebrew Tap (Recommended for ongoing releases) 8 | 9 | 1. **Create a tap repository** (one-time setup): 10 | ```bash 11 | # Create a new GitHub repo named "homebrew-floo" 12 | # Repository URL will be: https://github.com/YUX/homebrew-floo 13 | ``` 14 | 15 | 2. **Populate the tap**: 16 | ```bash 17 | git clone https://github.com/YUX/homebrew-floo 18 | cd homebrew-floo 19 | mkdir -p Formula 20 | cp packaging/homebrew/floo.rb Formula/ 21 | git add Formula/floo.rb 22 | git commit -m "Add Floo formula" 23 | git push 24 | ``` 25 | 26 | 3. **Update SHA256 checksums** after each release: 27 | ```bash 28 | # Download release artifacts 29 | wget https://github.com/YUX/floo/releases/download/v0.1.2/floo-aarch64-macos-m1.tar.gz 30 | wget https://github.com/YUX/floo/releases/download/v0.1.2/floo-x86_64-macos-haswell.tar.gz 31 | 32 | # Calculate checksums 33 | shasum -a 256 floo-aarch64-macos-m1.tar.gz 34 | shasum -a 256 floo-x86_64-macos-haswell.tar.gz 35 | 36 | # Update Formula/floo.rb with the checksums 37 | ``` 38 | 39 | 4. **Users install with**: 40 | ```bash 41 | brew tap YUX/floo 42 | brew install floo 43 | ``` 44 | 45 | ### Option 2: Homebrew Core (More exposure, stricter requirements) 46 | 47 | Submit a PR to [homebrew-core](https://github.com/Homebrew/homebrew-core): 48 | - Requires notable project (1000+ stars or significant usage) 49 | - Must meet [formula requirements](https://docs.brew.sh/Acceptable-Formulae) 50 | - See [Contributing Guide](https://docs.brew.sh/How-To-Open-a-Homebrew-Pull-Request) 51 | 52 | ## AUR (Arch Linux) 53 | 54 | ### Publishing to AUR 55 | 56 | 1. **Set up AUR account** (one-time): 57 | - Create account at https://aur.archlinux.org/register 58 | - Add SSH key to your AUR account 59 | - Configure git: `git config --global user.name "Your Name"` 60 | 61 | 2. **Clone the AUR package repository**: 62 | ```bash 63 | git clone ssh://aur@aur.archlinux.org/floo.git floo-aur 64 | cd floo-aur 65 | ``` 66 | 67 | 3. **Update package files** for each release: 68 | ```bash 69 | # Copy PKGBUILD 70 | cp packaging/aur/PKGBUILD . 71 | 72 | # Download release artifacts to calculate checksums 73 | wget https://github.com/YUX/floo/releases/download/v0.1.2/floo-x86_64-linux-gnu-haswell.tar.gz 74 | wget https://github.com/YUX/floo/releases/download/v0.1.2/floo-aarch64-linux-gnu.tar.gz 75 | 76 | # Calculate checksums 77 | sha256sum floo-x86_64-linux-gnu-haswell.tar.gz 78 | sha256sum floo-aarch64-linux-gnu.tar.gz 79 | 80 | # Update PKGBUILD with checksums and version 81 | 82 | # Generate .SRCINFO 83 | makepkg --printsrcinfo > .SRCINFO 84 | 85 | # Commit and push 86 | git add PKGBUILD .SRCINFO 87 | git commit -m "Update to version 0.1.2" 88 | git push 89 | ``` 90 | 91 | 4. **Users install with**: 92 | ```bash 93 | yay -S floo 94 | # or 95 | paru -S floo 96 | ``` 97 | 98 | ### Testing AUR Package Locally 99 | 100 | ```bash 101 | cd packaging/aur 102 | makepkg -si # Build and install locally 103 | ``` 104 | 105 | ## Other Package Managers 106 | 107 | ### Nix/NixOS 108 | 109 | Create a derivation in `nixpkgs`: 110 | - Fork [nixpkgs](https://github.com/NixOS/nixpkgs) 111 | - Add derivation to `pkgs/by-name/fl/floo/package.nix` 112 | - Submit PR 113 | 114 | ### APT (Debian/Ubuntu) 115 | 116 | #### Option 1: Automated Publishing (Recommended) 117 | 118 | Use the included scripts and GitHub Actions to automatically build and publish .deb packages. 119 | 120 | **One-time setup:** 121 | 122 | 1. **Create APT repository** (GitHub Pages): 123 | ```bash 124 | # Create a new GitHub repo named "floo-apt" 125 | # Enable GitHub Pages (Settings → Pages → Branch: main) 126 | ``` 127 | 128 | 2. **Add repository secret**: 129 | - Go to your main repo Settings → Secrets → Actions 130 | - Add `APT_REPO_TOKEN` with a personal access token (repo write permissions) 131 | 132 | 3. **Configure workflow**: 133 | - Edit `.github/workflows/publish-apt.yml` 134 | - Update repository name if needed (line 63) 135 | 136 | **For each release:** 137 | 138 | The workflow automatically runs when you create a GitHub release. You can also trigger manually: 139 | ```bash 140 | # Via GitHub Actions UI or CLI 141 | gh workflow run publish-apt.yml -f version=0.1.2 142 | ``` 143 | 144 | **Users install with:** 145 | ```bash 146 | curl -fsSL https://yux.github.io/floo-apt/pubkey.gpg | sudo gpg --dearmor -o /usr/share/keyrings/floo.gpg 147 | echo 'deb [signed-by=/usr/share/keyrings/floo.gpg] https://yux.github.io/floo-apt stable main' | sudo tee /etc/apt/sources.list.d/floo.list 148 | sudo apt update 149 | sudo apt install floo 150 | ``` 151 | 152 | #### Option 2: Manual Publishing 153 | 154 | **Build .deb packages:** 155 | ```bash 156 | cd packaging 157 | ./build-deb.sh 0.1.2 158 | ``` 159 | 160 | This creates: 161 | - `build-deb/floo_0.1.2-1_amd64.deb` 162 | - `build-deb/floo_0.1.2-1_arm64.deb` 163 | 164 | **Test locally:** 165 | ```bash 166 | sudo dpkg -i build-deb/floo_0.1.2-1_amd64.deb 167 | flooc --version 168 | ``` 169 | 170 | **Create APT repository:** 171 | ```bash 172 | ./setup-apt-repo.sh apt-repo 0.1.2 173 | ``` 174 | 175 | This generates a complete APT repository structure in `apt-repo/`. 176 | 177 | **Host the repository:** 178 | - Upload to GitHub Pages 179 | - Use services like [Cloudsmith](https://cloudsmith.io/) or [PackageCloud](https://packagecloud.io/) (free for open source) 180 | - Host on your own server 181 | 182 | #### Option 3: Submit to Debian/Ubuntu (Long-term) 183 | 184 | For official Debian/Ubuntu inclusion: 185 | - Requires Debian Developer or finding a sponsor 186 | - Package must meet [Debian Policy](https://www.debian.org/doc/debian-policy/) 187 | - See [Debian Maintainer Guide](https://www.debian.org/doc/manuals/maint-guide/) 188 | 189 | #### GPG Signing (Optional but Recommended) 190 | 191 | To sign your APT repository: 192 | 193 | 1. **Generate GPG key:** 194 | ```bash 195 | gpg --full-generate-key 196 | # Follow prompts, use your email 197 | ``` 198 | 199 | 2. **Export private key for GitHub Actions:** 200 | ```bash 201 | gpg --armor --export-secret-keys YOUR_KEY_ID > private-key.asc 202 | # Add contents as GPG_PRIVATE_KEY secret in GitHub 203 | ``` 204 | 205 | 3. **Manual signing:** 206 | ```bash 207 | cd apt-repo/dists/stable 208 | gpg --default-key YOUR_KEY_ID -abs -o Release.gpg Release 209 | gpg --default-key YOUR_KEY_ID --clearsign -o InRelease Release 210 | gpg --armor --export YOUR_KEY_ID > ../../pubkey.gpg 211 | ``` 212 | 213 | ### Snap (Universal Linux) 214 | 215 | #### Option 1: Automated Publishing (Recommended) 216 | 217 | Use GitHub Actions to automatically build and publish Snap packages. 218 | 219 | **One-time setup:** 220 | 221 | 1. **Register snap name:** 222 | ```bash 223 | snapcraft login 224 | snapcraft register floo 225 | ``` 226 | 227 | 2. **Export credentials:** 228 | ```bash 229 | snapcraft export-login --snaps=floo --channels=stable snapcraft-token.txt 230 | # Add contents as SNAPCRAFT_TOKEN secret in GitHub repo 231 | ``` 232 | 233 | 3. **Configure workflow:** 234 | - The workflow `.github/workflows/publish-snap.yml` is already configured 235 | - It runs automatically on each release 236 | 237 | **For each release:** 238 | 239 | Workflow automatically builds and publishes when you create a GitHub release. 240 | 241 | **Users install with:** 242 | ```bash 243 | sudo snap install floo 244 | ``` 245 | 246 | #### Option 2: Manual Building and Publishing 247 | 248 | **Build locally:** 249 | ```bash 250 | cd packaging 251 | ./build-snap.sh 0.1.2 252 | ``` 253 | 254 | **Test locally:** 255 | ```bash 256 | sudo snap install ../snap/floo_0.1.2_amd64.snap --dangerous 257 | floo.flooc --version 258 | floo.floos --version 259 | ``` 260 | 261 | **Publish manually:** 262 | ```bash 263 | cd snap 264 | snapcraft login 265 | snapcraft upload floo_0.1.2_amd64.snap --release=stable 266 | ``` 267 | 268 | #### Architecture Support 269 | 270 | - **amd64** - Uses Haswell-optimized x86_64 build 271 | - **arm64** - Uses baseline aarch64 build 272 | 273 | #### Snap Features 274 | 275 | - Strict confinement for security 276 | - Both `flooc` and `floos` included 277 | - Commands: `floo.flooc` and `floo.floos` 278 | - Service mode for `floos` (can run as daemon) 279 | - Example configs in `/snap/floo/current/share/doc/floo/examples/` 280 | 281 | See [snap/README.md](snap/README.md) for detailed usage instructions. 282 | 283 | ### RPM (Fedora/RHEL) 284 | 285 | Create `.spec` file for RPM packaging: 286 | - Use `rpmbuild` to create RPM packages 287 | - Consider COPR for hosting 288 | 289 | ## Release Checklist 290 | 291 | When releasing a new version: 292 | 293 | - [ ] Update version in `build.zig.zon` 294 | - [ ] Create git tag: `git tag v0.1.2 && git push --tags` 295 | - [ ] Wait for GitHub Actions to build release artifacts 296 | - [ ] Calculate SHA256 checksums: `./packaging/calculate-checksums.sh v0.1.2` 297 | - [ ] Update Homebrew formula with new version and checksums 298 | - [ ] Update AUR PKGBUILD with new version and checksums 299 | - [ ] Push to homebrew-floo tap 300 | - [ ] Push to AUR repository 301 | - [ ] APT packages build and publish automatically via GitHub Actions 302 | - [ ] Snap packages build and publish automatically via GitHub Actions 303 | - [ ] Update README.md installation instructions if needed 304 | 305 | ## Calculating Checksums 306 | 307 | Use the included helper script: 308 | 309 | ```bash 310 | ./packaging/calculate-checksums.sh v0.1.2 311 | ``` 312 | 313 | Or manually: 314 | 315 | ```bash 316 | # macOS 317 | shasum -a 256 floo-*.tar.gz 318 | 319 | # Linux 320 | sha256sum floo-*.tar.gz 321 | ``` 322 | -------------------------------------------------------------------------------- /src/udp_server.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const posix = std.posix; 3 | const net = @import("net_compat.zig"); 4 | const tunnel = @import("tunnel.zig"); 5 | const noise = @import("noise.zig"); 6 | const common = @import("common.zig"); 7 | 8 | /// Server-side UDP forwarder. 9 | /// Each tunnel stream gets an independent connected UDP socket so replies from 10 | /// the target can be associated with the originating client stream. 11 | pub const UdpForwarder = struct { 12 | allocator: std.mem.Allocator, 13 | service_id: tunnel.ServiceId, 14 | target_addr: net.Address, 15 | tunnel_conn: *anyopaque, 16 | send_fn: *const fn (conn: *anyopaque, buffer: []u8, payload_len: usize) anyerror!void, 17 | running: std.atomic.Value(bool), 18 | timeout_ns: i64, 19 | sessions: std.AutoHashMap(tunnel.StreamId, *Session), 20 | sessions_mutex: std.Thread.Mutex, 21 | 22 | pub fn create( 23 | allocator: std.mem.Allocator, 24 | service_id: tunnel.ServiceId, 25 | target_host: []const u8, 26 | target_port: u16, 27 | tunnel_conn: *anyopaque, 28 | send_fn: *const fn (conn: *anyopaque, buffer: []u8, payload_len: usize) anyerror!void, 29 | timeout_seconds: u64, 30 | ) !*UdpForwarder { 31 | const target_addr = try net.Address.resolveIp(target_host, target_port); 32 | const forwarder = try allocator.create(UdpForwarder); 33 | forwarder.* = .{ 34 | .allocator = allocator, 35 | .service_id = service_id, 36 | .target_addr = target_addr, 37 | .tunnel_conn = tunnel_conn, 38 | .send_fn = send_fn, 39 | .running = std.atomic.Value(bool).init(true), 40 | .timeout_ns = @as(i64, @intCast(timeout_seconds * std.time.ns_per_s)), 41 | .sessions = std.AutoHashMap(tunnel.StreamId, *Session).init(allocator), 42 | .sessions_mutex = .{}, 43 | }; 44 | return forwarder; 45 | } 46 | 47 | pub fn handleUdpData(self: *UdpForwarder, udp_msg: tunnel.UdpDataMsg) !void { 48 | if (udp_msg.source_addr.len == 0 or udp_msg.source_addr.len > 16) { 49 | return error.InvalidSourceAddress; 50 | } 51 | 52 | const now = @as(i64, @intCast(common.nanoTimestamp())); 53 | self.pruneExpiredSessions(now); 54 | 55 | const session = try self.ensureSession(udp_msg.stream_id, udp_msg.source_addr, udp_msg.source_port, now); 56 | 57 | _ = posix.send(session.socket_fd, udp_msg.data, 0) catch |err| { 58 | std.debug.print("[UDP-SERVER] send error for stream {}: {}\n", .{ udp_msg.stream_id, err }); 59 | return err; 60 | }; 61 | session.last_activity_ns.store(now, .release); 62 | } 63 | 64 | pub fn stop(self: *UdpForwarder) void { 65 | self.running.store(false, .release); 66 | var to_close = std.ArrayListUnmanaged(tunnel.StreamId){}; 67 | defer to_close.deinit(self.allocator); 68 | 69 | self.sessions_mutex.lock(); 70 | var iter = self.sessions.keyIterator(); 71 | while (iter.next()) |key_ptr| { 72 | if (to_close.append(self.allocator, key_ptr.*)) |_| {} else |_| break; 73 | } 74 | self.sessions_mutex.unlock(); 75 | 76 | for (to_close.items) |stream_id| { 77 | self.removeSession(stream_id, false); 78 | } 79 | } 80 | 81 | pub fn destroy(self: *UdpForwarder) void { 82 | self.stop(); 83 | self.sessions.deinit(); 84 | self.allocator.destroy(self); 85 | } 86 | 87 | const Session = struct { 88 | stream_id: tunnel.StreamId, 89 | socket_fd: posix.fd_t, 90 | thread: std.Thread, 91 | running: std.atomic.Value(bool), 92 | forwarder: *UdpForwarder, 93 | last_activity_ns: std.atomic.Value(i64), 94 | source_addr: [16]u8, 95 | source_addr_len: u8, 96 | source_port: u16, 97 | }; 98 | 99 | fn ensureSession( 100 | self: *UdpForwarder, 101 | stream_id: tunnel.StreamId, 102 | source_addr: []const u8, 103 | source_port: u16, 104 | now: i64, 105 | ) !*Session { 106 | self.sessions_mutex.lock(); 107 | if (self.sessions.get(stream_id)) |session| { 108 | defer self.sessions_mutex.unlock(); 109 | if (session.source_addr_len != source_addr.len or 110 | session.source_port != source_port or 111 | !std.mem.eql(u8, session.source_addr[0..session.source_addr_len], source_addr)) 112 | { 113 | return error.UdpForwarderBusy; 114 | } 115 | return session; 116 | } 117 | self.sessions_mutex.unlock(); 118 | 119 | const fd = try posix.socket(self.target_addr.any.family, posix.SOCK.DGRAM | posix.SOCK.CLOEXEC, 0); 120 | errdefer posix.close(fd); 121 | try posix.connect(fd, &self.target_addr.any, self.target_addr.getOsSockLen()); 122 | 123 | const session = try self.allocator.create(Session); 124 | session.* = .{ 125 | .stream_id = stream_id, 126 | .socket_fd = fd, 127 | .thread = undefined, 128 | .running = std.atomic.Value(bool).init(true), 129 | .forwarder = self, 130 | .last_activity_ns = std.atomic.Value(i64).init(now), 131 | .source_addr = [_]u8{0} ** 16, 132 | .source_addr_len = @intCast(source_addr.len), 133 | .source_port = source_port, 134 | }; 135 | std.mem.copyForwards(u8, session.source_addr[0..session.source_addr_len], source_addr); 136 | 137 | session.thread = std.Thread.spawn(.{ 138 | .stack_size = common.DEFAULT_THREAD_STACK, 139 | }, sessionRecvThread, .{session}) catch |err| { 140 | posix.close(fd); 141 | self.allocator.destroy(session); 142 | return err; 143 | }; 144 | 145 | self.sessions_mutex.lock(); 146 | self.sessions.put(stream_id, session) catch |err| { 147 | self.sessions_mutex.unlock(); 148 | session.running.store(false, .release); 149 | posix.shutdown(session.socket_fd, .recv) catch {}; 150 | session.thread.join(); 151 | posix.close(session.socket_fd); 152 | self.allocator.destroy(session); 153 | return err; 154 | }; 155 | self.sessions_mutex.unlock(); 156 | 157 | return session; 158 | } 159 | 160 | fn sessionRecvThread(session: *Session) void { 161 | var buf: [common.SOCKET_BUFFER_SIZE]u8 align(64) = undefined; 162 | const forwarder = session.forwarder; 163 | 164 | while (session.running.load(.acquire) and forwarder.running.load(.acquire)) { 165 | const n = posix.recv(session.socket_fd, &buf, 0) catch |err| { 166 | if (err == error.Interrupted) continue; 167 | break; 168 | }; 169 | if (n <= 0) continue; 170 | 171 | session.last_activity_ns.store(@as(i64, @intCast(common.nanoTimestamp())), .release); 172 | 173 | var encode_buf: [70016]u8 = undefined; 174 | const udp_msg = tunnel.UdpDataMsg{ 175 | .service_id = forwarder.service_id, 176 | .stream_id = session.stream_id, 177 | .source_addr = session.source_addr[0..session.source_addr_len], 178 | .source_port = session.source_port, 179 | .data = buf[0..n], 180 | }; 181 | 182 | const encoded_len = udp_msg.encodeInto(&encode_buf) catch { 183 | continue; 184 | }; 185 | 186 | forwarder.send_fn(forwarder.tunnel_conn, encode_buf[0 .. encoded_len + noise.TAG_LEN], encoded_len) catch |err| { 187 | std.debug.print("[UDP-SERVER] Failed to send to tunnel: {}\n", .{err}); 188 | }; 189 | } 190 | 191 | forwarder.removeSession(session.stream_id, true); 192 | } 193 | 194 | fn pruneExpiredSessions(self: *UdpForwarder, now: i64) void { 195 | if (self.timeout_ns == 0) return; 196 | 197 | // Workaround for Zig compiler bug in Debug mode on some platforms 198 | // Split into smaller parts to avoid genSetReg error 199 | var expired_items = std.ArrayListUnmanaged(tunnel.StreamId){}; 200 | defer expired_items.deinit(self.allocator); 201 | 202 | { 203 | self.sessions_mutex.lock(); 204 | defer self.sessions_mutex.unlock(); 205 | 206 | var iter = self.sessions.iterator(); 207 | while (iter.next()) |entry| { 208 | const session_ptr = entry.value_ptr.*; 209 | const last_activity = session_ptr.last_activity_ns.load(.acquire); 210 | const time_elapsed = now - last_activity; 211 | 212 | if (time_elapsed > self.timeout_ns) { 213 | const stream_id = entry.key_ptr.*; 214 | expired_items.append(self.allocator, stream_id) catch continue; 215 | } 216 | } 217 | } 218 | 219 | // Remove expired sessions outside the lock 220 | for (expired_items.items) |stream_id| { 221 | self.removeSession(stream_id, false); 222 | } 223 | } 224 | 225 | fn removeSession(self: *UdpForwarder, stream_id: tunnel.StreamId, caller_is_thread: bool) void { 226 | self.sessions_mutex.lock(); 227 | const entry = self.sessions.fetchRemove(stream_id); 228 | self.sessions_mutex.unlock(); 229 | 230 | if (entry) |removed| { 231 | const session = removed.value; 232 | session.running.store(false, .release); 233 | posix.shutdown(session.socket_fd, .recv) catch {}; 234 | if (!caller_is_thread) { 235 | session.thread.join(); 236 | } 237 | posix.close(session.socket_fd); 238 | self.allocator.destroy(session); 239 | std.debug.print("[UDP-SERVER] Session {} closed\n", .{stream_id}); 240 | } 241 | } 242 | }; 243 | -------------------------------------------------------------------------------- /src/protocol.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// Maximum frame size: 1 MB (reduced from 16MB to save memory - frames never exceed 64KB in practice) 4 | pub const MAX_FRAME_SIZE: u32 = 1 * 1024 * 1024; 5 | 6 | /// Frame represents a length-prefixed message. 7 | /// Wire format: [4-byte length (big-endian)][payload] 8 | pub const Frame = struct { 9 | payload: []const u8, 10 | 11 | /// Encode a frame into the provided buffer. 12 | /// Returns the total bytes written (4 + payload.len). 13 | /// Buffer must be at least 4 + payload.len bytes. 14 | pub fn encode(self: Frame, buffer: []u8) !usize { 15 | if (self.payload.len > MAX_FRAME_SIZE) { 16 | return error.FrameTooLarge; 17 | } 18 | 19 | const total_size = 4 + self.payload.len; 20 | if (buffer.len < total_size) { 21 | return error.BufferTooSmall; 22 | } 23 | 24 | // Write length as big-endian u32 25 | const len: u32 = @intCast(self.payload.len); 26 | std.mem.writeInt(u32, buffer[0..4], len, .big); 27 | 28 | // Copy payload 29 | @memcpy(buffer[4..total_size], self.payload); 30 | 31 | return total_size; 32 | } 33 | 34 | /// Allocate and encode a frame. 35 | /// Caller owns the returned memory. 36 | pub fn encodeAlloc(self: Frame, allocator: std.mem.Allocator) ![]u8 { 37 | if (self.payload.len > MAX_FRAME_SIZE) { 38 | return error.FrameTooLarge; 39 | } 40 | 41 | const total_size = 4 + self.payload.len; 42 | const buffer = try allocator.alloc(u8, total_size); 43 | errdefer allocator.free(buffer); 44 | 45 | _ = try self.encode(buffer); 46 | return buffer; 47 | } 48 | }; 49 | 50 | /// FrameDecoder handles buffered decoding of frames. 51 | /// Accumulates bytes until a complete frame is available. 52 | /// Uses offset tracking instead of copying for O(1) decode performance. 53 | pub const FrameDecoder = struct { 54 | allocator: std.mem.Allocator, 55 | buffer: []u8, 56 | read_pos: usize, 57 | write_pos: usize, 58 | 59 | const BUFFER_SIZE = 1024 * 1024; // 1MB buffer per decoder 60 | 61 | pub fn init(allocator: std.mem.Allocator) FrameDecoder { 62 | // Allocate buffer, return error if allocation fails (handled by caller) 63 | const buffer = allocator.alloc(u8, BUFFER_SIZE) catch { 64 | // Return empty decoder on allocation failure 65 | return FrameDecoder{ 66 | .allocator = allocator, 67 | .buffer = &[_]u8{}, 68 | .read_pos = 0, 69 | .write_pos = 0, 70 | }; 71 | }; 72 | return FrameDecoder{ 73 | .allocator = allocator, 74 | .buffer = buffer, 75 | .read_pos = 0, 76 | .write_pos = 0, 77 | }; 78 | } 79 | 80 | pub fn deinit(self: *FrameDecoder) void { 81 | if (self.buffer.len > 0) { 82 | self.allocator.free(self.buffer); 83 | } 84 | } 85 | 86 | /// Feed data into the decoder. 87 | pub fn feed(self: *FrameDecoder, data: []const u8) !void { 88 | // Compact if we don't have enough space 89 | const available_space = self.buffer.len - self.write_pos; 90 | if (data.len > available_space) { 91 | self.compact(); 92 | } 93 | 94 | // If still not enough space after compaction, error 95 | const space_after_compact = self.buffer.len - self.write_pos; 96 | if (data.len > space_after_compact) { 97 | return error.BufferFull; 98 | } 99 | 100 | // Append data at write position 101 | @memcpy(self.buffer[self.write_pos..][0..data.len], data); 102 | self.write_pos += data.len; 103 | } 104 | 105 | /// Try to decode the next frame. 106 | /// Returns null if not enough data is available yet. 107 | /// The returned slice borrows the decoder buffer and stays valid 108 | /// until the next call to `feed` or `reset`. 109 | pub fn decode(self: *FrameDecoder) !?[]const u8 { 110 | const available = self.write_pos - self.read_pos; 111 | 112 | // Need at least 4 bytes for length header 113 | if (available < 4) { 114 | return null; 115 | } 116 | 117 | // Read the length 118 | const len = std.mem.readInt(u32, self.buffer[self.read_pos..][0..4], .big); 119 | 120 | // Validate frame size 121 | if (len > MAX_FRAME_SIZE) { 122 | return error.FrameTooLarge; 123 | } 124 | 125 | const total_size = 4 + len; 126 | 127 | // Do we have the complete frame? 128 | if (available < total_size) { 129 | return null; // Not yet 130 | } 131 | 132 | // Extract the payload (skip the 4-byte length prefix) 133 | const payload = self.buffer[self.read_pos + 4 ..][0..len]; 134 | 135 | // Advance read position (O(1) operation!) 136 | self.read_pos += total_size; 137 | 138 | return payload; 139 | } 140 | 141 | /// Compact the buffer by moving unread data to the beginning. 142 | /// Only called when we need more space. 143 | fn compact(self: *FrameDecoder) void { 144 | const available = self.write_pos - self.read_pos; 145 | 146 | if (available == 0) { 147 | // Empty, just reset positions 148 | self.read_pos = 0; 149 | self.write_pos = 0; 150 | return; 151 | } 152 | 153 | // Move remaining unread data to beginning 154 | if (self.read_pos > 0) { 155 | const remaining = self.buffer[self.read_pos..self.write_pos]; 156 | std.mem.copyForwards(u8, self.buffer[0..remaining.len], remaining); 157 | self.read_pos = 0; 158 | self.write_pos = available; 159 | } 160 | } 161 | 162 | /// Reset the decoder, clearing all buffered data. 163 | pub fn reset(self: *FrameDecoder) void { 164 | self.read_pos = 0; 165 | self.write_pos = 0; 166 | } 167 | }; 168 | 169 | // Tests 170 | test "encode small frame" { 171 | const payload = "hello"; 172 | const frame = Frame{ .payload = payload }; 173 | 174 | var buffer: [1024]u8 = undefined; 175 | const size = try frame.encode(&buffer); 176 | 177 | try std.testing.expectEqual(@as(usize, 9), size); // 4 + 5 178 | try std.testing.expectEqual(@as(u32, 5), std.mem.readInt(u32, buffer[0..4], .big)); 179 | try std.testing.expectEqualSlices(u8, payload, buffer[4..9]); 180 | } 181 | 182 | test "encode empty frame" { 183 | const frame = Frame{ .payload = "" }; 184 | 185 | var buffer: [1024]u8 = undefined; 186 | const size = try frame.encode(&buffer); 187 | 188 | try std.testing.expectEqual(@as(usize, 4), size); 189 | try std.testing.expectEqual(@as(u32, 0), std.mem.readInt(u32, buffer[0..4], .big)); 190 | } 191 | 192 | test "encode frame too large" { 193 | const allocator = std.testing.allocator; 194 | 195 | // Create a payload that's too large 196 | const large_payload = try allocator.alloc(u8, MAX_FRAME_SIZE + 1); 197 | defer allocator.free(large_payload); 198 | 199 | const frame = Frame{ .payload = large_payload }; 200 | 201 | var buffer: [1024]u8 = undefined; 202 | try std.testing.expectError(error.FrameTooLarge, frame.encode(&buffer)); 203 | } 204 | 205 | test "encode buffer too small" { 206 | const payload = "hello world"; 207 | const frame = Frame{ .payload = payload }; 208 | 209 | var buffer: [10]u8 = undefined; // Too small 210 | try std.testing.expectError(error.BufferTooSmall, frame.encode(&buffer)); 211 | } 212 | 213 | test "decode single frame" { 214 | const allocator = std.testing.allocator; 215 | var decoder = FrameDecoder.init(allocator); 216 | defer decoder.deinit(); 217 | 218 | // Encode a frame 219 | const payload = "test message"; 220 | const frame = Frame{ .payload = payload }; 221 | var buffer: [1024]u8 = undefined; 222 | const size = try frame.encode(&buffer); 223 | 224 | // Feed it to decoder 225 | try decoder.feed(buffer[0..size]); 226 | 227 | // Decode 228 | const decoded = try decoder.decode(); 229 | try std.testing.expect(decoded != null); 230 | 231 | try std.testing.expectEqualSlices(u8, payload, decoded.?); 232 | } 233 | 234 | test "decode partial frame" { 235 | const allocator = std.testing.allocator; 236 | var decoder = FrameDecoder.init(allocator); 237 | defer decoder.deinit(); 238 | 239 | // Encode a frame 240 | const payload = "test message"; 241 | const frame = Frame{ .payload = payload }; 242 | var buffer: [1024]u8 = undefined; 243 | const size = try frame.encode(&buffer); 244 | 245 | // Feed only first 5 bytes 246 | try decoder.feed(buffer[0..5]); 247 | 248 | // Should not decode yet 249 | const decoded1 = try decoder.decode(); 250 | try std.testing.expect(decoded1 == null); 251 | 252 | // Feed the rest 253 | try decoder.feed(buffer[5..size]); 254 | 255 | // Now it should decode 256 | const decoded2 = try decoder.decode(); 257 | try std.testing.expect(decoded2 != null); 258 | 259 | try std.testing.expectEqualSlices(u8, payload, decoded2.?); 260 | } 261 | 262 | test "decode multiple frames" { 263 | const allocator = std.testing.allocator; 264 | var decoder = FrameDecoder.init(allocator); 265 | defer decoder.deinit(); 266 | 267 | // Encode two frames 268 | const payload1 = "first"; 269 | const payload2 = "second message"; 270 | 271 | const frame1 = Frame{ .payload = payload1 }; 272 | const frame2 = Frame{ .payload = payload2 }; 273 | 274 | var buffer: [1024]u8 = undefined; 275 | const size1 = try frame1.encode(buffer[0..]); 276 | const size2 = try frame2.encode(buffer[size1..]); 277 | 278 | // Feed both frames at once 279 | try decoder.feed(buffer[0..(size1 + size2)]); 280 | 281 | // Decode first frame 282 | const decoded1 = try decoder.decode(); 283 | try std.testing.expect(decoded1 != null); 284 | try std.testing.expectEqualSlices(u8, payload1, decoded1.?); 285 | 286 | // Decode second frame 287 | const decoded2 = try decoder.decode(); 288 | try std.testing.expect(decoded2 != null); 289 | try std.testing.expectEqualSlices(u8, payload2, decoded2.?); 290 | 291 | // No more frames 292 | const decoded3 = try decoder.decode(); 293 | try std.testing.expect(decoded3 == null); 294 | } 295 | 296 | test "decode frame too large" { 297 | const allocator = std.testing.allocator; 298 | var decoder = FrameDecoder.init(allocator); 299 | defer decoder.deinit(); 300 | 301 | // Create a fake header claiming an oversized frame 302 | var buffer: [4]u8 = undefined; 303 | std.mem.writeInt(u32, &buffer, MAX_FRAME_SIZE + 1, .big); 304 | 305 | try decoder.feed(&buffer); 306 | 307 | // Should error when trying to decode 308 | try std.testing.expectError(error.FrameTooLarge, decoder.decode()); 309 | } 310 | 311 | test "encodeAlloc" { 312 | const allocator = std.testing.allocator; 313 | 314 | const payload = "allocated frame"; 315 | const frame = Frame{ .payload = payload }; 316 | 317 | const encoded = try frame.encodeAlloc(allocator); 318 | defer allocator.free(encoded); 319 | 320 | try std.testing.expectEqual(@as(usize, 4 + payload.len), encoded.len); 321 | try std.testing.expectEqual(@as(u32, payload.len), std.mem.readInt(u32, encoded[0..4], .big)); 322 | try std.testing.expectEqualSlices(u8, payload, encoded[4..]); 323 | } 324 | --------------------------------------------------------------------------------