A bounded-resource Layer 4 (L4) proxy and embeddable Go networking runtime.
vmflow is a self-hosted, cross-platform TCP/UDP port forwarding tool written in pure Go. Run it as a single-binary network proxy or embed the forwarding runtime in your own Go control plane, with explicit resource limits, hot-reloadable rules, a terminal UI, and Prometheus metrics.
Documentation: Website · 中文 · Docs source
完整使用指南: 中文文档源码 —— 覆盖安装、配置、运维、安全加固与排错。English quick reference is below; the deep guide is in the public docs source.
Real TUI output from the current vmflow build.
- TCP, UDP, and
tcp+udpport forwarding - Per-rule IPv4/IPv6 source allowlists and denylists with CIDR support
- Configurable TCP connection limits and bounded UDP sessions, with rejection/drop counters
- Rule lifecycle management: start, stop, restart, and full snapshot apply
- Config-driven foreground process with hot reload
- Local CLI/TUI management for rules, stats, precheck, reload, and metrics
- Read-only stdio MCP server for local AI-assisted diagnostics
- Bearer-token auth with viewer/admin roles
- Structured logs in text or JSON format
- Prometheus-compatible
/metrics - Optional durable per-rule cumulative traffic and drop counters
- Rule precheck for loops, duplicate ports, and unavailable listeners
- Embeddable Go runtime for products that need in-process forwarding
- Terminal dashboard and rule management via
vmflow tui
- Forward TCP services such as SSH, databases, and internal APIs between hosts.
- Relay UDP services with bounded sessions, queues, idle cleanup, and drop metrics.
- Run a self-hosted Layer 4 proxy on Linux, macOS, or Windows as a native service.
- Inspect forwarding state and traffic from a local MCP client without exposing a new network service.
- Embed TCP/UDP forwarding into a Go application, VPS panel, or network control plane.
Install the current MCP-enabled prerelease and a colocated config (Linux/macOS):
VMFLOW_BIN_DIR="$(
curl -fsSL https://raw.githubusercontent.com/cloudapp3/vmflow/main/install.sh \
| bash -s -- --version v0.2.0-rc.4 --print-install-dir
)" \
&& [ -n "$VMFLOW_BIN_DIR" ] \
&& [ "${VMFLOW_BIN_DIR#/}" != "$VMFLOW_BIN_DIR" ] \
&& [ -x "$VMFLOW_BIN_DIR/vmflow" ] \
&& export PATH="$VMFLOW_BIN_DIR:$PATH"Omit --version v0.2.0-rc.4 to follow the latest stable release instead. The
unversioned installer currently resolves to v0.1.1, which predates the MCP
and Source IP policy features documented on this branch. The installer accepts
that release's legacy examples/config.yaml archive layout as well as the
top-level config.yaml layout used by current releases.
Without an explicit --dir, the installer first reuses a conventional existing
vmflow installation so upgrades keep its colocated config. A fresh root install
uses /usr/local/bin; an unprivileged user prefers ~/.local/bin or ~/bin
when either is already in PATH, then falls back to ~/.local/bin. For an
automatic home-directory install, the installer adds an idempotent entry to the
selected shell's common startup file (.zshrc, .bashrc, or .profile) and
prints the exact reload command; other shells receive a warning. The captured
directory and export above make the selected binary available in the current
shell without guessing its location.
The first install creates config.yaml beside the binary with mode 0600.
Reinstalling or upgrading replaces only the binary and preserves the existing
config. Running vmflow uses the colocated config by default; -config
overrides that path.
The bundled SSH forwarding example is disabled and listens on loopback only.
Review its listen address, target, and firewall exposure before setting
enabled: true; a first launch starts the control plane without exposing a new
forwarding port.
Start vmflow in one terminal:
vmflowQuery it from another terminal:
vmflow ctl rules
vmflow ctl stats
vmflow ctl metrics
vmflow ctl precheckOpen the terminal UI or show build metadata:
vmflow tui
vmflow version -jsonTo explicitly request a system installation, use --system. It writes directly
when already root and otherwise validates sudo before downloading, then uses
sudo only to prepare, inspect, and write the target directory:
curl -fsSL https://raw.githubusercontent.com/cloudapp3/vmflow/main/install.sh \
| bash -s -- --version v0.2.0-rc.4 --systemWhen a non-root user requests --system, a newly created colocated config stays
root-owned with mode 0600; the installer therefore prints sudo/root commands
for verification and startup. The default quick start above uses a per-user
install instead and makes vmflow directly runnable in the current shell.
The installer downloads GitHub Release archives, verifies checksums.txt with
SHA-256 by default, and installs vmflow plus, only when absent, a colocated
config.yaml. Override the automatic directory with --dir PATH or
VMFLOW_INSTALL_DIR; explicit directories never modify shell startup files.
Use --no-modify-path to disable startup-file changes for an automatic user
install, --print-install-dir for machine-readable PATH integration, and
--skip-verify to skip checksum verification if needed. For private releases
or higher GitHub API limits, set GITHUB_TOKEN or GH_TOKEN.
Or build from source:
git clone https://github.com/cloudapp3/vmflow.git
cd vmflow
go build -trimpath -o vmflow ./cmd/vmflow
cp -n examples/config.yaml config.yaml
./vmflowSee examples/config.yaml:
version: 1
control_port: 19090 # management always binds to 127.0.0.1
udp_max_sessions: 256 # all UDP sessions in this process
log:
level: info
format: text
stats:
persist: false # opt in to cumulative counter persistence
# path: /var/lib/vmflow/stats.json # optional; relative paths use the config dir
flush_interval: 60s # minimum 1s; requires restart when changed
auth:
enabled: false
tokens:
- name: admin
token: change-me
role: admin
rules:
- rule_id: ssh-forward
name: ssh-forward
protocol: tcp
listen_addr: 127.0.0.1
listen_port: 2201
target_addr: 127.0.0.1
target_port: 22
enabled: false
max_conn: 0 # TCP: unlimited; UDP: default of 256
source_ip_mode: allowlist # off, allowlist, or denylist
source_ips: # literal IPv4/IPv6 or CIDR; max 256
- 203.0.113.8
- 198.51.100.0/24
- 2001:db8:100::/48source_ip_mode controls admission independently for each forwarding rule.
With allowlist, only peers matching source_ips are accepted; with
denylist, matching peers are rejected. Omit the fields or use off to allow
all peers. A configured allowlist or denylist must contain at least one entry.
Invalid modes, hostnames, malformed addresses, empty entries, and lists larger
than 256 are rejected during startup and precheck. TCP is checked before the
connection consumes max_conn or dials the target. UDP is checked before a
session consumes per-rule or manager-wide capacity. Updating a source IP policy
restarts that rule and closes its established TCP connections and UDP sessions
so the new policy takes effect immediately.
The policy matches the socket peer address. Behind NAT or a Layer 4 proxy that address may be the gateway or proxy rather than the original client. vmflow does not trust forwarded HTTP headers or PROXY protocol metadata. This is an application-level admission control and does not replace a cloud firewall, security group, or host firewall for volumetric attack filtering.
udp_max_sessions limits active UDP sessions across every rule owned by the
process (default 256, maximum 4096) and can be changed by config reload.
Lowering it below current usage does not terminate established sessions; it
rejects new UDP sessions until usage falls below the new limit. For each UDP
rule, max_conn: 0 uses the default of 256; for TCP, max_conn: 0 remains
unlimited. On a tcp+udp rule, max_conn is enforced independently for TCP
connections and UDP sessions; UDP sessions also consume the process-wide limit.
Each UDP session owns a socket, a receive goroutine, and a 64 KiB receive
buffer. Check available memory and open-file limits before raising either cap.
Set stats.persist: true to preserve per-rule upload/download byte totals,
source-IP denial totals, and UDP rejection/drop counters across restarts. Active
connection counts and rates remain process-local. The foreground default is
stats.json beside the loaded config. The installed Linux systemd unit uses its
managed state directory
(/var/lib/vmflow/stats.json), including when running with --user vmflow.
An explicit relative stats.path is resolved beside the config file. vmflow
refuses to start if the stats path aliases the config file or cannot be written.
Hot reload applies only rules and udp_max_sessions. Changes to the local
management port, auth, TLS, logging, bot, ACME, certificate cache, certificate
review, or stats persistence require a vmflow process restart and are rejected
by reload while the old runtime settings remain active.
The management listener always binds to 127.0.0.1; control_port changes only
its port. Use an SSH tunnel when CLI/TUI access is needed from another host.
Existing control_listen_addr values that name a loopback address are accepted
for one migration release with a deprecation warning. Non-loopback legacy values
are rejected; replace the field with control_port before upgrading.
vmflow tui -token ADMIN_TOKEN can manage rules and udp_max_sessions when
auth is enabled and the token has the admin role. Viewer tokens and sessions
with auth disabled are read-only. In the Rules view use n/e/c to create,
edit, or copy, space to toggle, d to delete, g for the global UDP limit,
P to precheck, A to apply, and u to discard the draft.
The rule editor's Source IPs / CIDRs field accepts comma-separated entries.
Apply writes the validated draft to the config loaded by the running process,
which is the config.yaml beside the resolved vmflow binary by default (or the
explicit -config path). Auth, TLS, logging, and other process settings still
require editing YAML and restarting vmflow.
vmflow [-config path] [-control-port 19090]
vmflow ctl [-token TOKEN] <rules|stats|metrics|precheck|reload>
vmflow tui [-token TOKEN]
vmflow mcp [-token TOKEN]
vmflow version [-json]
vmflow update [--check] [--version tag]
vmflow service (install|uninstall|status) [--config path] [--binary path] [--user name] [--log-file path]
[--control-port port] [--extra-arg value]...
vmflow uninstall [--dry-run]Self-update is supported on Linux and macOS. On Windows, download and install the release ZIP manually.
Aliases are available: ctl=c, tui=t, version=v, update=u, and service=svc.
Register vmflow as a native OS service so it starts at boot and restarts on crash — one command on every platform:
sudo vmflow service install --config /usr/local/bin/config.yamlFor safety, service install refuses to register a service that points at a
relative path, a user-writable binary or config file, or either file under
user-writable parent directories (symlinks are resolved first). Install
vmflow into a protected root/admin-owned path such as
/usr/local/bin/vmflow, /opt/vmflow/vmflow, or
C:\Program Files\vmflow\vmflow.exe, and keep the service config in a protected
root/admin-owned location such as /usr/local/bin/config.yaml or
/etc/vmflow/config.yaml; pass --binary if you need to point at the installed
binary explicitly. A service running as a dedicated user needs explicit read
access to the root-owned config (for example, owner root, group vmflow, mode
0640).
The config is parsed before the OS service definition is changed. Running
service install again updates the existing definition and restarts the
service with the new settings.
- Linux: writes and reloads a systemd unit, enables it, restarts the service, and verifies it is active. Logs go to journald (
journalctl -u vmflow). The unit runs as root by default withCAP_NET_BIND_SERVICE(so it can bind privileged ports),Restart=on-failure, and a writable/var/lib/vmflowstate directory. Pass--user vmflowto run under a dedicated account (created if missing). - macOS: writes a launchd daemon (
KeepAliverestarts on crash) and bootstraps it. Logs land under/var/log/vmflow/(override with--log-file). - Windows: registers a Windows Service (
start=auto, restart-on-failure) visible inservices.msc. Because the SCM provides no stdout, logs default toC:\ProgramData\vmflow\logs\vmflow.log; override with--log-fileif needed.
Uninstall with sudo vmflow service uninstall (config and logs are left in place). Inspect with vmflow service status.
For a complete removal, run sudo vmflow uninstall. It prints the full plan
and requires confirmation before removing the service, binary, platform-default
config, installer-owned colocated config, persistent traffic statistics, logs,
update cache, and vmflow-owned certificate caches. Stats files are parsed and
revalidated immediately before removal; changed or unrecognized files are kept.
An unowned colocated config.yaml is preserved. External TLS certificate and key
files are never removed because they may be shared with other services. Custom
certificate cache directories are left in place unless the directory is
exclusively owned by vmflow and contains a .vmflow-owned marker; use
--dry-run to inspect the plan without changing the system.
For a user installation created by install.sh, run the installer-level
uninstall so it can also remove its exact PATH block from .zshrc, .bashrc,
or .profile after the binary cleanup succeeds:
curl -fsSL https://raw.githubusercontent.com/cloudapp3/vmflow/main/install.sh \
| bash -s -- --uninstallUse sudo bash -s -- --uninstall only for a root-owned system installation.
Re-running the installer-level uninstall also removes a stale installer-owned
PATH block when the vmflow files have already been deleted. Similar user-written
PATH lines and symlinked or non-writable shell startup files are left untouched.
The supported management interfaces are vmflow ctl, vmflow tui, and the
read-only vmflow mcp adapter. The daemon uses an internal loopback-only
control channel to implement them; that transport is not a public integration
API and carries no compatibility promise. control_port changes the local port
without making it externally reachable.
For remote administration, forward the loopback port over SSH and continue to use the CLI/TUI locally:
ssh -N -L 19090:127.0.0.1:19090 user@server
vmflow ctl rulesSource-IP policy denials are exposed in rule stats and as
vmflow_rule_source_ip_denied_total; the counter represents denied TCP
connections or UDP datagrams according to the rule protocol. UDP
admission-attempt failures and rate-limit queue-capacity drops are exposed as
vmflow_udp_session_rejected_total and
vmflow_udp_packets_dropped_total metrics. Manager-wide usage is exposed as
vmflow_udp_sessions_active and vmflow_udp_sessions_limit.
vmflow mcp starts a foreground, tools-only MCP server over stdio. It connects
to an already running local vmflow daemon and exits when the MCP client
disconnects. It does not start forwarding, listen on an MCP network port, or
modify the daemon configuration.
Available tools:
| Tool | Purpose |
|---|---|
get_vmflow_status |
Connection, version, authorization, rule-count, traffic, and degraded-state summary |
list_forwarding_rules |
Filtered rule summaries without endpoint or source-policy details |
get_forwarding_rule |
Full configuration, running state, and statistics for one rule |
get_traffic_stats |
Filtered and sorted per-rule counters plus aggregate totals |
run_config_precheck |
Read-only validation of the daemon's current persisted configuration |
Use a dedicated viewer token when daemon authentication is enabled. Prefer the
VMFLOW_CONTROL_TOKEN environment variable so the token is not visible in the
process command line:
auth:
enabled: true
tokens:
- name: mcp-viewer
token: replace-with-a-long-random-token
role: viewerClaude Desktop configuration:
{
"mcpServers": {
"vmflow": {
"command": "/usr/local/bin/vmflow",
"args": ["mcp"],
"env": {
"VMFLOW_CONTROL_TOKEN": "replace-with-a-long-random-token"
}
}
}
}Codex configuration:
[mcp_servers.vmflow]
command = "/usr/local/bin/vmflow"
args = ["mcp"]
env = { VMFLOW_CONTROL_TOKEN = "replace-with-a-long-random-token" }If control_port is not 19090, add "-addr", "http://127.0.0.1:PORT" to the client arguments. MCP management addresses are
restricted to localhost and loopback IPs. For a remote daemon, run vmflow mcp
on that host (for example through SSH) rather than exposing its management
listener.
Rule details contain target addresses, source IP policies, domains, and remarks; traffic and precheck results can also reveal local network topology. Tool results are sent to the model configured by the MCP client. The server exposes no write tools, raw configuration, bot tokens, certificate private keys, shell execution, file access, prompts, or resources. Precheck may resolve the targets already present in the daemon configuration.
vmflow can run an optional Telegram bot for querying and controlling forwarding from a chat. Configure it in config.yaml:
bot_token: "123456:ABC-DEF..." # Telegram bot token from @BotFather
bot_chat: 123456789 # your chat ID (from @userinfobot)
bot_control_token: "admin-xxx" # admin auth token; lets the bot write. Omit for read-only.The bot only responds to bot_chat (chat-ID allowlist). Private chats and groups are supported; confirmation results stay in the originating chat. Set bot_control_token to an admin auth.tokens entry to enable write commands; without it the bot is read-only. Bot control requires auth.enabled: true.
Commands:
/status,/rules,/detail <id>— read-only queries/reload— reload configuration from disk/stop <id>,/start_rule <id>,/toggle <id>— disable / enable / toggle a rule (persists toconfig.yamlwith precheck + optimistic locking)
Write commands use the same authenticated internal control path as the TUI and
vmflow ctl, so they get precheck, transactional apply with rollback,
optimistic concurrency, and audit logging. Concurrent configuration edits are
reported to the chat with a retry hint. Bot requests stay in-process.
You can also configure and control the bot at runtime from the TUI without editing config.yaml or restarting the daemon:
- Press
bin the Dashboard or Rules view to open the Bot panel (running state + current settings; tokens are masked). eeditsbot_token/bot_chat/bot_control_token(token fields are masked) —Ctrl+Sverifies the Bot token with Telegram, persists toconfig.yaml, and rebuilds the bot goroutine in place.s/xstart / stop the bot at runtime (not persisted; a daemon restart restores from config).rrefreshes.
Bot configuration changes use the same conflict-aware persistence path as rule edits. A rejected token leaves the existing bot and file untouched; a file commit failure restores the previous runtime bot. Daemon restart is not required and forwarding is not interrupted.
Use the top-level package when vmflow is embedded into another Go service:
rt := vmflow.New()
defer rt.Close()
result := rt.Apply(rules) // []engine.Rule
stats := rt.SnapshotAll()The embedding application owns persistence, auth, UI, audit logs, and business rules. vmflow owns only in-process forwarding, rule lifecycle, and real-time counters. See the embedding guide.
MIT. See THIRD_PARTY_NOTICES.md for bundled dependency licenses.
