Skip to main content

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 โ†’ local 10.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.salt and /etc/ferret/db.header must 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 reset to 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 Distribution

ferret_agent-0.1.2.tar.gz (103.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ferret_agent-0.1.2-py3-none-any.whl (109.8 kB view details)

Uploaded Python 3

File details

Details for the file ferret_agent-0.1.2.tar.gz.

File metadata

  • Download URL: ferret_agent-0.1.2.tar.gz
  • Upload date:
  • Size: 103.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for ferret_agent-0.1.2.tar.gz
Algorithm Hash digest
SHA256 ce3b24509ff1bfe6fba28a163729ae43ae6331f41fbf66d66d6f949d8090194c
MD5 decc423c0b327ef5fe1d779a9760b0dc
BLAKE2b-256 6ed19dbaea4276850ee2172c82f0ea976826e4519487c5c98d786809a5f7e397

See more details on using hashes here.

File details

Details for the file ferret_agent-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: ferret_agent-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 109.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for ferret_agent-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 69ce57aa22ce1fa5c1a08aa03f022920863cdc3a73b1af39a55c0e9e14cb7a6b
MD5 84229e4ba574e52b0d0b1f0e0af911bd
BLAKE2b-256 6949a90acba67cfd8cf001084a363f20cb35cb481f8ee81af4dcf36caedb64b4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page