Ferret โ self-hosted outbound tunnel agent. No port forwarding, no third-party cloud.
Project description
๐พ Ferret
โ ๏ธ v0.1 Alpha โ works in production for us, but APIs and config format may change before v1.0. See Roadmap.
Self-hosted outbound tunnel. No port forwarding. No third-party cloud. No accounts.
Ferret lets you reach any device behind a firewall โ corporate NAT, hospital network, hotel WiFi โ without opening a single inbound port. The agent initiates an outbound connection on port 443. That's it.
The problem
You have a Raspberry Pi, a camera, a printer, a PLC โ behind a strict firewall you don't control.
- WireGuard requires both sides to have reachable ports. Doesn't work.
- Tailscale / ZeroTier work, but your traffic goes through their servers. They require an account. You pay monthly. You trust them.
- ngrok is a relay. Same story.
Ferret is different: you run the server. Your traffic never leaves your infrastructure.
And your server doesn't need a static IP. Run it at home with a No-IP or any dynamic DNS service โ the agent connects to yourname.ddns.net and it just works.
How it works
Your device (behind firewall) Your server (VPS, cloud, on-prem)
โโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ferret agent โ โ ferret-server โ
โ โ outbound โ โ
โ TUN interface โโโโโโโโโโผโโโโโโโโโโโโโผโโโโ WebSocket on :443 โ
โ LAN: 192.168.1.0/24 โ โ โ
โ Cameras, printers... โ โ Admin UI ยท REST API โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโ โ Per-agent ACL โ
โ Audit log โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The agent creates a TUN interface and connects outbound. The server assigns a VPN IP and routes traffic to the agent's LAN. Cameras, printers, PLCs โ all reachable, no changes needed on those devices.
Features
- Outbound only โ agent initiates, works through any firewall
- Full LAN access โ reach every device on the agent's network, not just the agent
- Native subnet replication โ via NETMAP, the remote LAN appears on a local subnet; devices are reachable as if you were physically on the same network (e.g. remote
192.168.1.xโ local10.8.0.x, same structure, no manual IP mapping) - Per-agent ACL โ restrict which ports/IPs are accessible, hot-reloadable without disconnect
- Invite tokens โ 5-minute short-lived tokens; HWID auto-bound on first connect; admin approves
- Hardware binding โ client HWID and server HWID both bound into token via HMAC; stolen token unusable on different hardware
- Token rotation โ versioned secrets (
kid), retire old keys after natural expiry - Rate limiting โ IP-based backoff on failed connection attempts
- Audit log โ every connection, proxy open, rule change, token event โ filterable UI
- One-line installer โ server generates a bash/PowerShell script with all credentials embedded
- TCP proxy โ tunnel RDP, SSH, VNC to any host on agent's LAN
- Encrypted storage โ LUKS container bound to server hardware; stolen server cannot be decrypted without the original machine
- Self-hosted โ your server, your data, zero external dependencies
Quick start
Server (static IP or dynamic DNS)
pip install "ferret-agent[server]"
ferret-server \
--admin-token your-secret-token \
--secret your-signing-secret \
--public-url https://yourname.ddns.net
Only port 443 needs to be open. The server listens on :: by default (dual-stack IPv4+IPv6). To bind IPv4 only: --host 0.0.0.0. A static IP is not required โ dynamic DNS (No-IP, DuckDNS) works fine, as long as the server has a real public IP. CGNAT does not work โ if your ISP shares a public IP across multiple customers, run the server on a VPS instead.
Open https://yourname.ddns.net/agents/ui โ admin UI with token management and agent status.
Agent (manual)
pip install ferret-agent
# Create agent.conf (or use the installer generated by the server UI)
cat > agent.conf << EOF
url = wss://your-server.example.com/ws/agent
token = <token from server UI>
EOF
ferret
Agent (one-line installer)
In the server UI, click ๐ง next to any agent to download a self-contained bash script that installs Python, ferret-agent, writes config, and registers a systemd service.
curl https://your-server.example.com/agents/<token>/installer?os=linux | sudo bash
VPN client (ferret tun)
Connect your laptop to any agent's LAN from anywhere โ the remote network appears locally via NETMAP:
pip install ferret-agent
sudo ferret tun \
--server https://your-server.example.com \
--admin-token your-secret-token \
--agent <agent-token>
After connecting, the remote LAN is directly reachable โ ping 10.8.0.100, SSH, browser, everything works as if you were physically on site. Requires root / CAP_NET_ADMIN.
To run as a systemd service:
sudo tee /etc/systemd/system/ferret-tun.service << EOF
[Unit]
Description=Ferret TUN VPN
After=network-online.target
[Service]
ExecStart=/usr/local/bin/ferret tun --server https://your-server.example.com --admin-token your-secret-token --agent <agent-token>
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable --now ferret-tun
Token management
Standard token
Created via API or admin UI. Valid immediately, no expiry by default.
# Via CLI
ferret-server token create "Office router" --rules "tcp:22,tcp:3389"
# Via API
curl -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-X POST https://your-server/agents/tokens \
-d '{"name": "Office router", "rules": ["tcp:22", "tcp:3389"]}'
Invite token (5-minute, HWID-bound)
For deploying agents securely without pre-sharing a permanent token. The invite is valid for 5 minutes โ just long enough for the agent to make its first connection. After first connect, the server records the agent's hardware fingerprint (HWID). On every subsequent connection, the HWID must match.
Admin creates invite (5 min)
โ
โผ
Agent connects โ server records HWID
โ
โผ
Admin approves in UI (sees HWID, confirms it's the right machine)
โ
โผ
Agent: permanent, bound to that hardware forever
If the invite expires before first use, it's dead โ create a new one. If someone else gets the invite token but connects from a different machine, admin sees the wrong HWID and rejects it.
# API
curl -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-X POST https://your-server/agents/invite \
-d '{"name": "Branch office", "rules": ["tcp:22"]}'
# Returns token valid for 5 minutes
# Agent uses it like any token
ferret --token INVITE_TOKEN --server wss://your-server/ws/agent
# After connecting, waits for admin approval
Token rotation
Retire old signing secrets without disconnecting existing agents. Old tokens remain valid until they expire naturally.
from ferret.server.token_gen import TokenGen
gen = TokenGen(
secrets={"v1": "old-secret-32chars+", "v2": "new-secret-32chars+"},
active="v2"
)
# New tokens signed with v2; existing v1 tokens still validate
gen.retire("v1") # only after all v1 tokens have expired
Server HWID binding
Tokens can be bound to a specific server's hardware. Even if an attacker steals both the database and the signing secret, tokens cannot be replayed on a different server.
gen.create(
hw_id="client-hardware-fingerprint",
name="Branch office",
server_hw_id="server-hardware-fingerprint", # from ferret.hw_id.get() on server
)
Access control (ACL)
Per-agent rules. Empty rules = agent cannot open any proxy tunnel.
"tcp:22" โ SSH only
"tcp:3389" โ RDP only
"udp:161" โ SNMP only
"tcp:*" โ all TCP
"udp:*" โ all UDP
"*:*" โ everything (TCP + UDP)
Rules are hot-reloadable โ change takes effect on the next proxy request, no reconnect needed.
# Update rules via API
curl -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-X PUT https://your-server/agents/TOKEN/rules \
-d '{"rules": ["tcp:22", "tcp:443"]}'
TCP proxy (RDP, SSH, VNC)
Reach any host on the agent's LAN from your server:
from ferret.server import AgentRouter
from ferret.example import PortalTcpProxy
router = AgentRouter(...)
proxy = PortalTcpProxy(router)
proxy.expose("agent-token", "192.168.1.50", 3389, local_port=13389) # RDP
proxy.expose("agent-token", "192.168.1.1", 22, local_port=12222) # SSH
Then mstsc /v:your-server:13389 from anywhere.
Encrypted server storage (ferret-db)
ferret-db protects server data from physical theft. The LUKS container is bound to hardware fingerprint (HWID) โ a key derived from DMI UUIDs, CPU model, and disk serials. Without the original hardware, the container is unreadable.
How it works
Hardware (DMI + CPU + disk serials)
โ
โผ
HWID string โโโโ SHA-256 โโโโโบ material
machine.key โโโโโโโโโโโโโโโโโโบ material
โ
โผ
HKDF(SHA-256, salt, info="ferret-db-key-v1") โ LUKS passphrase (64 bytes)
โ
โผ
cryptsetup luksOpen /db/ferret-data.img
โ
โผ
/db/secure/ โ ext4, mounted at boot, unmounted at shutdown
Key is derived in memory via memfd_create (never touches disk). Memory is mlock-ed and zeroed after use.
Setup (one-time)
# Install with crypto dependencies
pip install "ferret-agent[all]"
# Initialize 20GB container
sudo ferret-db init --size 20
# Output:
# Container : /db/ferret-data.img (20GB)
# Header : /etc/ferret/db.header โ back this up separately!
# Mounted : /db/secure
The LUKS header is stored separately at /etc/ferret/db.header. Back it up โ without it the container cannot be opened even with the correct hardware.
Critical:
/etc/ferret/db.saltand/etc/ferret/db.headermust be real files on the filesystem โ not symlinks pointing inside/db/secure. The unlock process reads the salt to derive the key before the container is open. Placing the salt inside the container creates a circular dependency and makes the server unbootable.
Daily operation
sudo ferret-db unlock # open + mount (called automatically by systemd)
sudo ferret-db lock # unmount + close
sudo ferret-db status # check state
sudo ferret-db reset # re-key after hardware or network change (audit logged)
Note: The network fingerprint includes the first reachable WAN hop IP. If your ISP changes their routing and this IP changes, the container will fail to unlock after reboot. Run
sudo ferret-db resetto bind to the new network topology.
systemd integration
[Service]
PermissionsStartOnly=true
ExecStartPre=/usr/local/bin/ferret-db unlock
ExecStart=/usr/local/bin/ferret-server --admin-token ... --secret ...
ExecStopPost=/usr/local/bin/ferret-db lock
See example/ferret-server.service.example for the full unit file.
Security properties
| Property | Detail |
|---|---|
| Key source | DMI UUID + board/chassis serial + CPU model + disk serials + network fingerprint |
| Hardware minimum | 2 hardware identifiers required; fails safe otherwise |
| Network fingerprint | Default gateway IP, local subnet, gateway MAC (ARP), first WAN hop (traceroute TTL=2) |
| Key derivation | HKDF-SHA256, 64-byte output, random 32-byte salt per init |
| In-memory key | memfd_create + mlock โ key never written to disk or swap |
| Audit log | Append-only (chattr +a) JSON log at /var/log/ferret-db-audit.log |
| Header separation | LUKS header at /etc/ferret/db.header, body at /db/ferret-data.img |
Security model
| Layer | Mechanism |
|---|---|
| Transport | TLS 1.3 (WebSocket over HTTPS) |
| Token signing | HMAC-SHA256, kid-versioned, supports rotation + retire |
| Client HWID | Token stores HMAC(secret, hw_id) โ plain fingerprint never in token |
| Server HWID | Token stores HMAC(secret, server_hw_id) โ token invalid on different hardware |
| HWID binding | Invite tokens bind HWID on first connect; enforced on all subsequent connects |
| Application crypto | ChaCha20-Poly1305 AEAD, HKDF-SHA256 session keys, per-session nonce |
| Access control | Per-agent ACL rules, hot-reloadable, deny-by-default |
| Rate limiting | IP-based failed-attempt counter, exponential backoff |
| Audit | SQLite log of every event, filterable UI at /agents/audit |
| Server storage | LUKS container, HWID-bound key, memfd_create + mlock |
Audit events: agent_connect, agent_disconnect, proxy_open, proxy_deny, tun_start, tun_stop, token_create, token_revoke, rules_change, invite_create, agent_approve, admin_access
No traffic passes through third-party infrastructure. Ever.
Comparison
| Tailscale | ZeroTier | ngrok | Ferret | |
|---|---|---|---|---|
| Self-hosted | Partial | Partial | No | Yes |
| Account required | Yes | Yes | Yes | No |
| Traffic through their servers | Sometimes | Sometimes | Yes | Never |
| Firewall bypass | Partial | Partial | Yes | Yes |
| Full LAN access | No | No | No | Yes |
| Per-agent ACL | Enterprise | No | No | Yes |
| Audit log | Enterprise | No | Paid | Included |
| Hardware binding | No | No | No | Yes |
| Static IP required | Yes | Yes | โ | No |
| Dynamic DNS support | No | No | โ | Yes |
| Open source (server) | No | No | No | Yes |
| Price | $18/user/mo | $4/user/mo | $8+/mo | $0 |
Handlers
| Handler | Description |
|---|---|
TunHandler |
Layer 3 VPN, full LAN access via MASQUERADE |
ProxyHandler |
TCP proxy to any host:port on agent's LAN |
AmiHandler |
Asterisk AMI โ originate calls, query extensions |
SmsHandler |
GSM dongle SMS via Asterisk DongleSendSMS |
AiHandler |
Ollama Vision/OCR โ base64 image + prompt โ response |
Custom handlers in 5 lines:
@agent.on("my_command", capability="my_module")
async def handle(data: dict, send):
await send({"type": "my_response", "result": do_work(data)})
Installation
# Agent only (lightweight, no server deps)
pip install ferret-agent
# Server + agent
pip install "ferret-agent[server]"
# With crypto support
pip install "ferret-agent[all]"
Requires Python 3.11+. Linux only for TUN features; proxy and custom handlers work on any OS.
Roadmap
- v0.1 โ Python, WebSocket, all features above โ
โ CLI VPN client (
ferret tun); GUI tray app planned - v0.2 โ TAP mode (Layer 2 bridge) โ remote machines appear in Windows File Explorer Network neighborhood; Samba shares browsable without manual IP
- v0.3 โ QUIC transport (replaces WebSocket/TCP โ eliminates TCP-over-TCP, ~800 Mbps)
- v0.4 โ Rust rewrite of data plane (agent + server tunnel core)
- v0.5 โ Linux kernel module (
ferret.ko) โ WireGuard-class throughput - v1.0 โ Upstream Linux kernel submission
Origin
Ferret was born out of a real production need while building ServisPort โ a field service management platform for repair shops, started in May 2026.
We needed to connect field technicians' devices behind strict firewalls without opening ports or relying on third-party services.
Nothing on the market solved this simply. So we built it.
First commit: June 2026
Author: Dejan Kociฤ โ dkocic@servisport.org
License
Server component: SSPL-1.0 โ free for self-hosted use; commercial use requires a license.
Agent component: AGPLv3 โ free for everyone.
See LICENSE.
Contributing
Issues and PRs welcome. For security vulnerabilities, email directly (see profile).
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ferret_agent-0.1.4-py3-none-any.whl.
File metadata
- Download URL: ferret_agent-0.1.4-py3-none-any.whl
- Upload date:
- Size: 111.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
44464e3bed68673d5db30db9f356f54c593c1125d9719a9d49933b74b8d05404
|
|
| MD5 |
e014514a7f90c8cd20d453d9a4c3c4b9
|
|
| BLAKE2b-256 |
6cf0fe458c06366fad9659d62f51f0171191365f7ec2c12e6b1f84c031f5c65b
|