Jesse A. Green / notebook

frp (Fast Reverse Proxy) — User Guide

Generated May 2026 from fatedier/frp at e8dfd6e

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

frp is a high-performance reverse proxy application that exposes local servers behind NAT or firewalls to the public internet. It consists of two binaries: frps (the server, run on a public host) and frpc (the client, run on the machine behind NAT). Written in Go, frp supports TCP, UDP, HTTP, HTTPS, and several secret/P2P tunnel modes, with TOML as the preferred configuration format.

Note

This guide covers frp as of commit e8dfd6e. Configuration format is TOML (preferred). The legacy INI format is still accepted but deprecated — migrate to TOML.


Table of Contents

  1. Quick Start
  2. Installation
  3. Configuration
  4. Core Usage
  5. Common Tasks
  6. Troubleshooting
  7. FAQ

1. Quick Start

This section gets frp working end-to-end in under five minutes: a server on a public host and a client on the machine behind NAT, exposing SSH.

Prerequisites

  • A public server (VPS, cloud VM, etc.) with a reachable IP address or hostname.
  • A local machine behind NAT or a firewall where the service you want to expose is running.
  • Both machines must be able to reach each other on the frps bind port (default 7000).

Step 1 — Run frps on the public server

Create frps.toml:

bindPort = 7000

Start the server:

./frps -c frps.toml

Expected output:

2026/05/13 00:00:00 [I] [root.go:102] frps uses config file: frps.toml
2026/05/13 00:00:00 [I] [service.go:225] frps tcp listen on 0.0.0.0:7000
2026/05/13 00:00:00 [I] [root.go:123] frps started successfully

Step 2 — Run frpc on the local machine

Create frpc.toml (replace x.x.x.x with your server's public IP):

serverAddr = "x.x.x.x"
serverPort = 7000

[[proxies]]
name = "ssh"
type = "tcp"
localIP = "127.0.0.1"
localPort = 22
remotePort = 6000

Start the client:

./frpc -c frpc.toml

Expected output:

2026/05/13 00:00:00 [I] [root.go:142] frpc uses config file: frpc.toml
2026/05/13 00:00:00 [I] [service.go:287] try to connect to server...
2026/05/13 00:00:00 [I] [service.go:279] [ssh] start proxy success

Step 3 — Connect

ssh -p 6000 user@x.x.x.x

That's it. Traffic on the public server's port 6000 is now forwarded to port 22 on the local machine.

Tip

Always set an auth.token in production. The minimal config above has no authentication — anyone can connect to your frps and register proxies. See Authentication for how to add a shared secret.


2. Installation

2.1 Download Pre-built Binaries

Pre-built binaries for Linux, macOS, Windows, and common architectures are published on the GitHub Releases page.

Linux (amd64)

wget https://github.com/fatedier/frp/releases/latest/download/frp_linux_amd64.tar.gz
tar -xzf frp_linux_amd64.tar.gz
cd frp_linux_amd64/
# Binaries: frps, frpc

Linux (arm64)

wget https://github.com/fatedier/frp/releases/latest/download/frp_linux_arm64.tar.gz
tar -xzf frp_linux_arm64.tar.gz
cd frp_linux_arm64/

macOS (arm64 / Apple Silicon)

wget https://github.com/fatedier/frp/releases/latest/download/frp_darwin_arm64.tar.gz
tar -xzf frp_darwin_arm64.tar.gz
cd frp_darwin_arm64/

macOS (amd64)

wget https://github.com/fatedier/frp/releases/latest/download/frp_darwin_amd64.tar.gz
tar -xzf frp_darwin_amd64.tar.gz
cd frp_darwin_amd64/

Windows (amd64)

Download frp_windows_amd64.zip from the releases page, extract, and run frps.exe / frpc.exe from a command prompt or PowerShell.

2.2 Build from Source

Requires Go 1.25+.

git clone https://github.com/fatedier/frp.git
cd frp
make
# Produces: bin/frps, bin/frpc

Note

Docker container build issues have been reported (GitHub issue #5288). Building from source on the host or using the official pre-built binaries is the most reliable approach.

2.3 Docker

Official images are published to Docker Hub as fatedier/frps and fatedier/frpc.

Server:

docker run -d \
  --name frps \
  -p 7000:7000 \
  -v /path/to/frps.toml:/etc/frp/frps.toml \
  fatedier/frps -c /etc/frp/frps.toml

Client:

docker run -d \
  --name frpc \
  --network host \
  -v /path/to/frpc.toml:/etc/frp/frpc.toml \
  fatedier/frpc -c /etc/frp/frpc.toml

Tip

Use --network host for frpc when it needs to reach services on 127.0.0.1 of the Docker host. Without it, localIP = "127.0.0.1" points inside the container, not the host.

2.4 Verify Installation

./frps -v
./frpc -v

2.5 Validate a Config File

Before starting, verify your config syntax:

./frps verify -c frps.toml
./frpc verify -c frpc.toml

2.6 Running as a System Service

Linux (systemd) — frps:

Create /etc/systemd/system/frps.service:

[Unit]
Description=frp server
After=network.target

[Service]
Type=simple
User=nobody
ExecStart=/usr/local/bin/frps -c /etc/frp/frps.toml
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now frps
sudo systemctl status frps

Linux (systemd) — frpc:

[Unit]
Description=frp client
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/frpc -c /etc/frp/frpc.toml
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now frpc

3. Configuration

3.1 Config Format

frp uses TOML as the preferred format. Files are conventionally named frps.toml and frpc.toml. Legacy INI is still parsed but is deprecated — you should migrate.

Pass the config file with the -c flag:

./frps -c /etc/frp/frps.toml
./frpc -c /etc/frp/frpc.toml

3.2 Strict Mode

By default, unknown config keys are silently ignored. Enable strict mode to surface typos and misconfigurations:

./frps -c frps.toml --strict_config
./frpc -c frpc.toml --strict_config

Warning

--strict_config will refuse to start if there are any unrecognized keys. Always use it during initial setup to catch typos.

3.3 Server Configuration (frps.toml)

Minimal

bindPort = 7000

Full reference

# Network
bindAddr = "0.0.0.0"       # Interface to listen on (default: "0.0.0.0")
bindPort = 7000             # Main frp port (default: 7000)
kcpBindPort = 7000          # KCP UDP port (only if using KCP transport)
quicBindPort = 7002         # QUIC UDP port (only if using QUIC transport)

# Virtual Host (HTTP/HTTPS proxies require these)
vhostHTTPPort = 80          # Port for HTTP virtual hosting
vhostHTTPSPort = 443        # Port for HTTPS virtual hosting
vhostHTTPTimeout = 60       # HTTP response header timeout (seconds)

# Subdomain routing
subdomainHost = "frp.example.com"  # Wildcard parent domain for subdomain routing

# TCP multiplexing
tcpmuxHTTPConnectPort = 443 # Port for tcpmux HTTP CONNECT proxies

# Authentication
[auth]
method = "token"            # "token" or "oidc" (default: "token")
token = "your-secret-token"
additionalScopes = []       # ["HeartBeats", "NewWorkConns"]

# Logging
[log]
to = "console"              # "console" or file path (default: "console")
level = "info"              # "trace","debug","info","warn","error" (default: "info")
maxDays = 3                 # Log file rotation (default: 3)
disablePrintColor = false

# Web Dashboard
[webServer]
addr = "0.0.0.0"
port = 7500                 # Dashboard port (default: 7500)
user = "admin"
password = "admin"
enableHTTPS = false

# Transport
[transport]
tcpMux = true               # HTTP/2 multiplexing over single TCP conn (default: true)
tcpMuxKeepaliveInterval = 30
maxPoolCount = 5            # Max pre-created connections per client (default: 5)
heartbeatTimeout = -1       # -1 = disabled when tcpMux=true (default: -1)
tls.force = false           # Require clients to use TLS

# Proxy limits
maxPortsPerClient = 0       # 0 = unlimited
allowPorts = []             # e.g. [{single=6000},{range={start=8000,end=9000}}]

3.4 Client Configuration (frpc.toml)

Minimal

serverAddr = "x.x.x.x"
serverPort = 7000

[[proxies]]
name = "my-proxy"
type = "tcp"
localIP = "127.0.0.1"
localPort = 8080
remotePort = 9090

Full reference

# Server connection
serverAddr = "x.x.x.x"     # frps host (required)
serverPort = 7000           # frps port (default: 7000)
natHoleStunServer = "stun.easyvoip.com:3478"  # STUN server for XTCP

# Authentication
[auth]
method = "token"
token = "your-secret-token"

# Logging
[log]
to = "console"
level = "info"
maxDays = 3

# Web Admin API
[webServer]
addr = "127.0.0.1"
port = 7400                 # Admin API port (default: 7400)
user = "admin"
password = "admin"

# Transport
[transport]
protocol = "tcp"            # "tcp","kcp","quic","websocket","wss" (default: "tcp")
dialServerTimeout = 10      # Connection timeout in seconds (default: 10)
dialServerKeepalive = 7200  # Keepalive interval in seconds (default: 7200)
poolCount = 1               # Pre-created connection pool size (default: 1)
tcpMux = true               # Must match server setting (default: true)
tcpMuxKeepaliveInterval = 30
proxyURL = ""               # HTTP proxy for frpc→frps connection
heartbeatInterval = -1      # -1 when tcpMux=true (default: -1)
heartbeatTimeout = -1
bandwidthLimit = "1MB"      # Per-proxy bandwidth cap, e.g. "1MB", "500KB"
bandwidthLimitMode = "client"  # "client" or "server" (default: "client")
tls.enable = true           # TLS for frpc→frps channel (default: true since v0.50.0)
tls.disableCustomTLSFirstByte = true  # (default: true since v0.50.0)
tls.serverName = ""
tls.certFile = ""
tls.keyFile = ""
tls.trustedCaFile = ""

# UDP packet buffer size (default: 1500)
udpPacketSize = 1500

# Login behavior
loginFailExit = true        # Exit if initial login fails (default: true)

# Proxies and visitors are defined below as [[proxies]] and [[visitors]]

3.5 Authentication {#authentication}

Token (default)

The simplest method. Set the same token on both sides:

# frps.toml
[auth]
method = "token"
token = "strong-random-secret"
# frpc.toml
[auth]
method = "token"
token = "strong-random-secret"

Token from file or command:

# frpc.toml
[auth]
method = "token"
[auth.tokenSource]
type = "file"
path = "/run/secrets/frp-token"
[auth.tokenSource]
type = "exec"
command = "cat"
args = ["/run/secrets/frp-token"]

OIDC (OpenID Connect)

For enterprise or cloud setups where you manage identity through an IdP:

# frpc.toml
[auth]
method = "oidc"
[auth.oidc]
clientID = "frp-client"
clientSecret = "secret"
audience = "frp-server"
scope = "openid"
tokenEndpointURL = "https://idp.example.com/oauth/token"
# frps.toml
[auth]
method = "oidc"
[auth.oidc]
issuer = "https://idp.example.com"
audience = "frp-server"

Note

With OIDC you can also restrict which operations require a valid token via additionalScopes = ["HeartBeats", "NewWorkConns"] on both sides.

3.6 Logging

[log]
to = "/var/log/frp/frps.log"  # File path or "console"
level = "info"                # trace / debug / info / warn / error
maxDays = 7                   # Rotate and keep for N days
disablePrintColor = true      # Disable ANSI colors (useful for log files)

3.7 TLS

Since v0.50.0, TLS between frpc and frps is enabled by default (transport.tls.enable = true). For custom certificates:

# frpc.toml
[transport.tls]
enable = true
certFile = "/etc/frp/client.crt"
keyFile  = "/etc/frp/client.key"
trustedCaFile = "/etc/frp/ca.crt"
serverName = "frps.example.com"
# frps.toml — force clients to present a valid TLS cert
[transport.tls]
force = true
certFile = "/etc/frp/server.crt"
keyFile  = "/etc/frp/server.key"
trustedCaFile = "/etc/frp/ca.crt"

Caution

Setting transport.tls.force = false on the server while clients use TLS has produced unexpected behavior (GitHub issue #5131). If you want plaintext connections, explicitly set tls.enable = false on the client side as well, and do not mix TLS and non-TLS clients on the same server.

3.8 Transport Protocols

The transport.protocol option on the client controls how frpc connects to frps:

Protocol Description When to use
tcp Default. Reliable TCP connection General use
kcp UDP-based, lower latency High-latency / lossy networks
quic QUIC (UDP), modern multiplexing High-throughput, modern networks
websocket TCP wrapped in WebSocket (ws://) Behind HTTP proxies/firewalls
wss TLS + WebSocket (wss://) Behind HTTPS proxies/firewalls
# frpc.toml — use KCP
[transport]
protocol = "kcp"
# frps.toml — open a KCP port
kcpBindPort = 7000

For QUIC:

# frpc.toml
[transport]
protocol = "quic"
# frps.toml
quicBindPort = 7002

QUIC defaults: keepalivePeriod=10s, maxIdleTimeout=30s, maxIncomingStreams=100000.

Tip

Set QUIC_GO_DISABLE_ECN=1 in the environment if your network drops ECN-marked packets, which can cause QUIC connections to stall.

3.9 Bandwidth Limiting

Limit bandwidth per proxy on the client side:

[[proxies]]
name = "limited-web"
type = "tcp"
localPort = 8080
remotePort = 9090
[proxies.transport]
bandwidthLimit = "2MB"
bandwidthLimitMode = "client"  # or "server"

4. Core Usage

This section covers what you can actually do with frp, organized by intent.

4.1 Expose a TCP Service

Use type = "tcp" to forward any TCP port. The most common use cases are SSH, databases, and custom TCP servers. Requires remotePort.

[[proxies]]
name = "ssh"
type = "tcp"
localIP = "127.0.0.1"
localPort = 22
remotePort = 6000

Connect:

ssh -p 6000 user@frps-server

Tip

You can expose a database (e.g., PostgreSQL on port 5432) the same way — just change localPort and remotePort. For maximum security, use stcp instead of tcp to avoid opening any public port at all.

4.2 Expose a UDP Service

Use type = "udp" for DNS, game servers, VoIP, or any UDP-based service. Requires remotePort.

[[proxies]]
name = "dns"
type = "udp"
localIP = "127.0.0.1"
localPort = 53
remotePort = 6053

Test:

dig @frps-server -p 6053 example.com

Note

UDP forwarding has known limitations with packet reassembly for very large payloads (GitHub issue #5289). The default udpPacketSize = 1500 matches typical MTU — increase it in both frpc and frps configs if your application sends larger UDP datagrams.

4.3 Expose an HTTP Application (Virtual Hosting)

Use type = "http" to expose web applications with virtual host routing. The frps server must have vhostHTTPPort set.

frps.toml:

bindPort = 7000
vhostHTTPPort = 80

frpc.toml:

[[proxies]]
name = "web"
type = "http"
localIP = "127.0.0.1"
localPort = 8080
customDomains = ["www.example.com"]

Now requests to http://www.example.com (hitting frps on port 80) are forwarded to your local app.

Using subdomains (requires subdomainHost on frps):

# frps.toml
subdomainHost = "example.com"
# frpc.toml
[[proxies]]
name = "app"
type = "http"
localPort = 3000
subdomain = "myapp"  # accessible at myapp.example.com

HTTP proxy advanced features:

[[proxies]]
name = "web-advanced"
type = "http"
localPort = 8080
customDomains = ["app.example.com"]
locations = ["/api", "/static"]           # Only proxy these path prefixes
httpUser = "basicuser"                     # Basic auth enforced by frps
httpPassword = "basicpass"
hostHeaderRewrite = "internal-hostname"    # Rewrite Host header sent to local app
routeByHTTPUser = true                     # Route to this proxy based on HTTP basic auth user
[proxies.requestHeaders.set]
  X-From-Frp = "true"
[proxies.responseHeaders.set]
  X-Served-By = "frp"

4.4 Expose an HTTPS Application

Use type = "https" when your local app handles TLS itself, or when you want frps to terminate TLS. Requires vhostHTTPSPort on frps.

Pass-through (local app handles TLS):

# frps.toml
vhostHTTPSPort = 443
# frpc.toml
[[proxies]]
name = "secure-web"
type = "https"
localIP = "127.0.0.1"
localPort = 443
customDomains = ["secure.example.com"]

For TLS termination and protocol conversion, see the Client Plugins section — specifically https2http and https2https.

4.5 TCP Multiplexing (tcpmux)

type = "tcpmux" allows multiple services to share a single port via HTTP CONNECT tunneling. Useful when you have a limited number of public ports.

# frps.toml
tcpmuxHTTPConnectPort = 443
# frpc.toml
[[proxies]]
name = "service-a"
type = "tcpmux"
multiplexer = "httpconnect"
localIP = "127.0.0.1"
localPort = 8080
customDomains = ["service-a.example.com"]

[[proxies]]
name = "service-b"
type = "tcpmux"
multiplexer = "httpconnect"
localIP = "127.0.0.1"
localPort = 8081
customDomains = ["service-b.example.com"]

Clients connect using CONNECT service-a.example.com:443 HTTP/1.1 and frps routes to the correct backend.

4.6 Secret Proxies (No Public Port)

For cases where you want to expose a service to specific trusted clients only, without opening a public port on frps.

stcp — Secret TCP

The service registers a named proxy with a secretKey. Only visitors with the matching key can connect — no public port is opened on frps.

frpc.toml (on the machine with the service):

[[proxies]]
name = "private-ssh"
type = "stcp"
localIP = "127.0.0.1"
localPort = 22
secretKey = "strong-shared-secret"
allowUsers = ["visitor-client-name"]   # Optional allowlist of frpc login names

frpc.toml (on the machine that wants to connect):

[[visitors]]
name = "ssh-visitor"
type = "stcp"
serverName = "private-ssh"            # Must match proxy name above
secretKey = "strong-shared-secret"
bindAddr = "127.0.0.1"
bindPort = 6000

Connect from the visitor machine:

ssh -p 6000 user@127.0.0.1

sudp — Secret UDP

Same pattern as stcp, for UDP services:

frpc.toml (service side):

[[proxies]]
name = "private-dns"
type = "sudp"
localIP = "127.0.0.1"
localPort = 53
secretKey = "shared-secret"

frpc.toml (visitor side):

[[visitors]]
name = "dns-visitor"
type = "sudp"
serverName = "private-dns"
secretKey = "shared-secret"
bindAddr = "127.0.0.1"
bindPort = 6053

xtcp — P2P TCP via NAT Hole Punching

xtcp attempts a direct peer-to-peer connection between two clients behind NAT, using the frps server only for signaling. This maximizes throughput and minimizes server load.

frpc.toml (service side):

[[proxies]]
name = "p2p-ssh"
type = "xtcp"
localIP = "127.0.0.1"
localPort = 22
secretKey = "shared-secret"
allowUsers = ["visitor-name"]

frpc.toml (visitor side):

[[visitors]]
name = "p2p-ssh-visitor"
type = "xtcp"
serverName = "p2p-ssh"
secretKey = "shared-secret"
bindAddr = "127.0.0.1"
bindPort = 6000
keepTunnelOpen = false          # Keep tunnel alive even when idle
maxRetriesAnHour = 8            # Max hole-punch retries per hour
minRetryInterval = 90           # Minimum seconds between retries
fallbackTo = "stcp-fallback"    # Fallback visitor name if hole punch fails
fallbackTimeoutMs = 2000        # Milliseconds before falling back

Note

xtcp throughput may be lower than standard TCP proxying in some NAT configurations (GitHub issue #5281). The STUN server (natHoleStunServer) must be reachable from both clients for NAT hole punching to work. Default: stun.easyvoip.com:3478.

Tip

Always configure a fallbackTo stcp visitor so that if NAT hole punching fails (e.g., symmetric NAT on either side), traffic automatically routes through frps instead with no manual intervention.

4.7 Client Plugins {#47-client-plugins}

Client plugins intercept traffic between frpc and your local service, enabling protocol translation, authentication injection, TLS handling, and more. All plugins are configured with [proxies.plugin].

http_proxy — Forward HTTP Proxy

Turn frp into a shared HTTP/HTTPS forward proxy:

[[proxies]]
name = "http-fwd"
type = "tcp"
remotePort = 8888
[proxies.plugin]
type = "http_proxy"
httpUser = "proxyuser"
httpPassword = "proxypass"

Use it:

curl -x http://proxyuser:proxypass@frps-server:8888 http://example.com

socks5 — SOCKS5 Proxy

[[proxies]]
name = "socks5-fwd"
type = "tcp"
remotePort = 1080
[proxies.plugin]
type = "socks5"
username = "user"
password = "pass"

Use it with any SOCKS5-compatible client:

curl --socks5 user:pass@frps-server:1080 http://example.com

static_file — Serve Local Files Over HTTP

Serve a directory of files to the public:

[[proxies]]
name = "file-server"
type = "tcp"
remotePort = 8080
[proxies.plugin]
type = "static_file"
localPath = "/home/user/public"
stripPrefix = "static"     # Strip /static prefix from URLs
httpUser = "viewer"
httpPassword = "secret"

Access: http://frps-server:8080/ (with basic auth credentials)

unix_domain_socket — Expose a Unix Domain Socket

[[proxies]]
name = "docker-sock"
type = "tcp"
remotePort = 2375
[proxies.plugin]
type = "unix_domain_socket"
unixPath = "/var/run/docker.sock"

Warning

Exposing /var/run/docker.sock publicly gives full Docker control to anyone who can connect. Always protect this with an auth.token and consider using stcp instead of tcp to avoid any public port exposure.

http2https — HTTP → HTTPS

Convert incoming plain HTTP to HTTPS for a local service:

[[proxies]]
name = "h2s"
type = "http"
customDomains = ["app.example.com"]
[proxies.plugin]
type = "http2https"
localAddr = "127.0.0.1:8443"
hostHeaderRewrite = "app.example.com"
[proxies.plugin.requestHeaders.set]
  X-From-Frp = "true"

https2http — HTTPS → HTTP (TLS Termination)

frpc terminates TLS and forwards plain HTTP to the local service:

[[proxies]]
name = "s2h"
type = "https"
customDomains = ["app.example.com"]
[proxies.plugin]
type = "https2http"
localAddr = "127.0.0.1:8080"
crtPath = "/etc/frp/server.crt"
keyPath  = "/etc/frp/server.key"
hostHeaderRewrite = "app.example.com"
enableHTTP2 = true

https2https — HTTPS → HTTPS (Cert Swap)

Replace the certificate seen by the public while keeping the backend connection encrypted:

[[proxies]]
name = "cert-swap"
type = "https"
customDomains = ["app.example.com"]
[proxies.plugin]
type = "https2https"
localAddr = "127.0.0.1:8443"
crtPath = "/etc/frp/public.crt"
keyPath  = "/etc/frp/public.key"
enableHTTP2 = true

http2http — HTTP → HTTP (Header Rewriting)

Transparently proxy HTTP while rewriting headers:

[[proxies]]
name = "http-rewrite"
type = "http"
customDomains = ["app.example.com"]
[proxies.plugin]
type = "http2http"
localAddr = "127.0.0.1:8080"
hostHeaderRewrite = "internal.app"
[proxies.plugin.requestHeaders.set]
  X-Real-IP = "$remote_addr"

tls2raw — Strip TLS, Forward Raw

frpc terminates the TLS layer and forwards raw bytes to a local TCP service:

[[proxies]]
name = "tls-strip"
type = "tcp"
remotePort = 9000
[proxies.plugin]
type = "tls2raw"
localAddr = "127.0.0.1:9001"
crtPath = "/etc/frp/server.crt"
keyPath  = "/etc/frp/server.key"

virtual_net — Virtual Network Interface

Enables a virtual network (layer 3) interface between frp nodes. Requires the VirtualNet feature gate.

[[proxies]]
name = "vnet-proxy"
type = "xtcp"
localIP = "127.0.0.1"
localPort = 0
[proxies.plugin]
type = "virtual_net"

Note

virtual_net is an advanced feature that enables IP-level routing between frp nodes. See the Admin Guide for architecture details and feature gate configuration.

4.8 Web Dashboard and Admin API

frps Dashboard

frps has a built-in web dashboard for monitoring connections:

# frps.toml
[webServer]
addr = "0.0.0.0"
port = 7500
user = "admin"
password = "admin"

Open http://frps-server:7500 in a browser. The dashboard shows:

  • Connected clients
  • Active proxies and their traffic statistics
  • System status

Warning

The dashboard has no authentication if you omit user/password. Always set credentials, or bind to 127.0.0.1 and access via SSH tunnel: ssh -L 7500:localhost:7500 root@frps-server. Then open http://localhost:7500.

frpc Admin API

frpc exposes a local HTTP API for live management:

# frpc.toml
[webServer]
addr = "127.0.0.1"
port = 7400
user = "admin"
password = "admin"
Endpoint Method Auth Description
/healthz GET No Health check
/api/reload GET Yes Reload config file without restart
/api/stop POST Yes Stop the frpc process
/api/status GET Yes Current proxy status
/api/config GET Yes Return current running config
/api/config PUT Yes Replace running config
/api/proxy/{name}/config GET Yes Config for a named proxy
/api/visitor/{name}/config GET Yes Config for a named visitor

Examples:

# Check health
curl http://admin:admin@127.0.0.1:7400/healthz

# Reload config after editing frpc.toml
curl http://admin:admin@127.0.0.1:7400/api/reload

# Check proxy status
curl http://admin:admin@127.0.0.1:7400/api/status | jq .

# Stop frpc gracefully
curl -X POST http://admin:admin@127.0.0.1:7400/api/stop

4.9 Health Checks

Configure health checks so frp automatically removes unhealthy proxies from the pool (especially useful with load balancing):

TCP health check:

[[proxies]]
name = "web-with-hc"
type = "tcp"
localPort = 8080
remotePort = 9090
[proxies.healthCheck]
type = "tcp"
timeoutSeconds = 3      # (default: 3)
maxFailed = 1           # Failed checks before removal (default: 1)
intervalSeconds = 10    # Check interval (default: 10)

HTTP health check:

[[proxies]]
name = "api-with-hc"
type = "http"
localPort = 8080
customDomains = ["api.example.com"]
[proxies.healthCheck]
type = "http"
path = "/healthz"
timeoutSeconds = 5
maxFailed = 3
intervalSeconds = 15
[proxies.healthCheck.httpHeaders]
  X-Health-Check = "frp"

4.10 Load Balancing

Distribute traffic across multiple backend instances by assigning proxies to a group:

# On backend machine 1 (frpc instance A)
[[proxies]]
name = "web-1"
type = "http"
localPort = 8080
customDomains = ["lb.example.com"]
[proxies.loadBalancer]
group = "web-cluster"
groupKey = "shared-lb-secret"

# On backend machine 2 (frpc instance B)
[[proxies]]
name = "web-2"
type = "http"
localPort = 8080
customDomains = ["lb.example.com"]
[proxies.loadBalancer]
group = "web-cluster"
groupKey = "shared-lb-secret"

Note

Load balancing is available for tcp, http, and tcpmux proxy types. All proxies in a group must share the same groupKey.

4.11 SSH Tunnel Gateway

frps supports a built-in SSH tunnel gateway that allows users to register proxies without running frpc — using only standard ssh commands. This is ideal for CI/CD pipelines, ephemeral tunnels, or environments where installing frpc is not practical.

Enable on frps:

# frps.toml
[sshTunnelGateway]
bindPort = 2200
privateKeyFile = "/etc/frp/ssh_host_rsa_key"
autoGenPrivateKeyPath = "./.autogen_ssh_key"  # Auto-generate if privateKeyFile is missing
authorizedKeysFile = "/etc/frp/authorized_keys"

Create a TCP tunnel via SSH:

ssh -R :6000:localhost:22 v0@frps-server -p 2200

This registers a tcp proxy with remotePort=6000 forwarding to local port 22.

HTTP tunnel:

ssh -R myapp.example.com:80:localhost:3000 v0@frps-server -p 2200

Supported proxy types via SSH gateway: tcp, http, https, tcpmux, stcp, sudp

Tip

The SSH gateway is perfect for development workflows: expose a local dev server with a single ssh command, no frpc binary or config file required. The tunnel lives as long as the SSH session is open.


5. Common Tasks

5.1 Workflow Recipes

Recipe 1: Expose SSH from a Home Server

# frps.toml (on public server)
bindPort = 7000
[auth]
token = "my-secret"
# frpc.toml (on home server)
serverAddr = "my-vps.example.com"
serverPort = 7000
[auth]
token = "my-secret"

[[proxies]]
name = "home-ssh"
type = "tcp"
localIP = "127.0.0.1"
localPort = 22
remotePort = 2222
ssh -p 2222 [email removed]

Recipe 2: Expose Multiple Web Apps on One Server

# frps.toml
bindPort = 7000
vhostHTTPPort = 80
subdomainHost = "frp.example.com"
[auth]
token = "my-secret"
# frpc.toml
serverAddr = "my-vps.example.com"
serverPort = 7000
[auth]
token = "my-secret"

[[proxies]]
name = "blog"
type = "http"
localPort = 4000
subdomain = "blog"

[[proxies]]
name = "api"
type = "http"
localPort = 8080
subdomain = "api"

[[proxies]]
name = "app"
type = "http"
localPort = 3000
subdomain = "app"

Results: http://blog.frp.example.com, http://api.frp.example.com, http://app.frp.example.com


Recipe 3: Secure Database Access Without a Public Port

# On the database machine (stcp service side)
[[proxies]]
name = "postgres"
type = "stcp"
localIP = "127.0.0.1"
localPort = 5432
secretKey = "db-secret-key"
allowUsers = ["dev-machine"]
# On the developer machine (stcp visitor)
[[visitors]]
name = "postgres-visitor"
type = "stcp"
serverName = "postgres"
secretKey = "db-secret-key"
bindAddr = "127.0.0.1"
bindPort = 15432
psql -h 127.0.0.1 -p 15432 -U dbuser mydb

No public port is opened. The database is reachable only by visitors with the correct secretKey.


Recipe 4: Share a Local Dev Server with a Teammate in Real Time

[[proxies]]
name = "devserver"
type = "http"
localPort = 3000
customDomains = ["dev.example.com"]

The teammate opens http://dev.example.com and sees the developer's local app live.


Recipe 5: Expose a DNS Server

[[proxies]]
name = "pihole"
type = "udp"
localIP = "127.0.0.1"
localPort = 53
remotePort = 5353
dig @my-vps.example.com -p 5353 google.com

Recipe 6: Access Home Network via SOCKS5 from Anywhere

# frpc.toml (on home router / server)
[[proxies]]
name = "socks5-home"
type = "tcp"
remotePort = 1080
[proxies.plugin]
type = "socks5"
username = "homeuser"
password = "homepass"
curl --socks5 homeuser:[email removed]:1080 http://192.168.1.100/

All traffic is routed through the home network.


Recipe 7: Serve a Local Directory Publicly

[[proxies]]
name = "files"
type = "tcp"
remotePort = 8080
[proxies.plugin]
type = "static_file"
localPath = "/home/user/share"
httpUser = "user"
httpPassword = "secret"

Access: http://my-vps.example.com:8080/ with credentials.


Recipe 8: Expose Docker Socket Securely via stcp

# On the Docker host
[[proxies]]
name = "docker"
type = "stcp"
secretKey = "docker-secret"
[proxies.plugin]
type = "unix_domain_socket"
unixPath = "/var/run/docker.sock"
# On the CI machine (visitor)
[[visitors]]
name = "docker-visitor"
type = "stcp"
serverName = "docker"
secretKey = "docker-secret"
bindAddr = "127.0.0.1"
bindPort = 2375
DOCKER_HOST=tcp://127.0.0.1:2375 docker ps

Recipe 9: Resilient P2P Tunnel with Automatic Fallback

# frpc.toml (visitor side)

# Primary: P2P via NAT hole punching
[[visitors]]
name = "p2p-ssh"
type = "xtcp"
serverName = "remote-ssh"
secretKey = "shared-secret"
bindAddr = "127.0.0.1"
bindPort = 6000
fallbackTo = "fallback-ssh"
fallbackTimeoutMs = 3000

# Fallback: route through frps if P2P fails
[[visitors]]
name = "fallback-ssh"
type = "stcp"
serverName = "remote-ssh-fallback"
secretKey = "shared-secret"
bindAddr = "127.0.0.1"
bindPort = 6001

Recipe 10: HTTP Basic Auth for an Unauthenticated Local Service

Your local app has no authentication. Add it at the frp layer:

[[proxies]]
name = "protected-app"
type = "http"
localPort = 8080
customDomains = ["app.example.com"]
httpUser = "admin"
httpPassword = "strongpassword"

frps challenges incoming requests with HTTP 401 before forwarding. The local app never sees unauthenticated requests.


Recipe 11: TLS Termination for a Plain HTTP Local App

# frps.toml
vhostHTTPSPort = 443
# frpc.toml
[[proxies]]
name = "tls-term"
type = "https"
customDomains = ["secure.example.com"]
[proxies.plugin]
type = "https2http"
localAddr = "127.0.0.1:8080"
crtPath = "/etc/ssl/secure.example.com.crt"
keyPath  = "/etc/ssl/secure.example.com.key"

Public clients connect via HTTPS; frpc strips TLS and forwards HTTP to the local app.


Recipe 12: CI/CD Webhook Receiver

Expose a local webhook handler during development:

[[proxies]]
name = "webhook"
type = "http"
localPort = 3001
customDomains = ["hooks.example.com"]

Configure GitHub/GitLab/Stripe to send webhooks to http://hooks.example.com/webhook. Your local handler receives them in real time.


Recipe 13: KCP Transport for High-Latency Networks

# frps.toml
kcpBindPort = 7001
# frpc.toml
serverAddr = "my-vps.example.com"
serverPort = 7001
[transport]
protocol = "kcp"

All proxy definitions remain identical — only the transport changes.


Recipe 14: Bandwidth-Throttled Public Service

[[proxies]]
name = "throttled-api"
type = "tcp"
localPort = 8080
remotePort = 9090
[proxies.transport]
bandwidthLimit = "500KB"
bandwidthLimitMode = "client"

Recipe 15: Multiple Proxies with Health Checks

serverAddr = "x.x.x.x"
serverPort = 7000
[auth]
token = "secret"

[[proxies]]
name = "web"
type = "http"
localPort = 8080
customDomains = ["www.example.com"]
[proxies.healthCheck]
type = "http"
path = "/health"
intervalSeconds = 10

[[proxies]]
name = "api"
type = "tcp"
localPort = 9000
remotePort = 19000
[proxies.healthCheck]
type = "tcp"
intervalSeconds = 15

[[proxies]]
name = "ssh"
type = "tcp"
localPort = 22
remotePort = 2222

Recipe 16: Hot Reload Config Without Restart

# Edit frpc.toml while frpc is running, then:
curl http://admin:admin@127.0.0.1:7400/api/reload

New proxy definitions are applied immediately without dropping existing connections.


Recipe 17: Expose a Game Server (UDP + TCP)

[[proxies]]
name = "game-tcp"
type = "tcp"
localPort = 27015
remotePort = 27015

[[proxies]]
name = "game-udp"
type = "udp"
localPort = 27015
remotePort = 27015

Recipe 18: Restrict Which Remote Ports frps Allows

# frps.toml — only allow ports 6000-6010 and 8080
[[allowPorts]]
single = 8080
[[allowPorts]]
range.start = 6000
range.end = 6010

frps rejects any proxy registration requesting a port outside this list.


Recipe 19: WebSocket Transport (Behind an HTTP Corporate Proxy)

# frpc.toml — when corporate firewall only allows HTTP/HTTPS
serverAddr = "my-vps.example.com"
serverPort = 80
[transport]
protocol = "websocket"

For HTTPS-only environments:

serverPort = 443
[transport]
protocol = "wss"

Recipe 20: Monitor All Connections via frps Dashboard Securely

# frps.toml — bind dashboard to localhost only
[webServer]
addr = "127.0.0.1"
port = 7500
user = "admin"
password = "strongadminpassword"
# SSH tunnel to access it from your local machine
ssh -L 7500:localhost:7500 [email removed]
# Open http://localhost:7500 in browser

Recipe 21: Expose Minecraft Server

[[proxies]]
name = "minecraft"
type = "tcp"
localPort = 25565
remotePort = 25565

Players connect to my-vps.example.com:25565.


Recipe 22: SSH Tunnel Gateway — Ephemeral Dev Tunnel Without frpc Binary

# One command on any machine with SSH — no frpc installed
ssh -R :8080:localhost:3000 [email removed] -p 2200

# Share http://my-vps.example.com:8080 with teammates
# Tunnel closes when you close the SSH session

Recipe 23: Restrict per-Client Port Quota

# frps.toml
maxPortsPerClient = 5

Each authenticated client can register at most 5 proxies.


Recipe 24: Load-Balanced HTTP with Health Checks

# frpc instance 1
[[proxies]]
name = "app-1"
type = "http"
localPort = 8080
customDomains = ["app.example.com"]
[proxies.loadBalancer]
group = "app-pool"
groupKey = "lb-secret"
[proxies.healthCheck]
type = "http"
path = "/health"

# frpc instance 2
[[proxies]]
name = "app-2"
type = "http"
localPort = 8080
customDomains = ["app.example.com"]
[proxies.loadBalancer]
group = "app-pool"
groupKey = "lb-secret"
[proxies.healthCheck]
type = "http"
path = "/health"

Unhealthy instances are automatically removed from the pool.


Recipe 25: Expose a Jupyter Notebook

[[proxies]]
name = "jupyter"
type = "tcp"
remotePort = 8888
[proxies.plugin]
type = "http_proxy"
httpUser = "labuser"
httpPassword = "labpass"

Or directly (if Jupyter has its own auth):

[[proxies]]
name = "jupyter"
type = "tcp"
localPort = 8888
remotePort = 8888

Recipe 26: Route HTTP Traffic by Path

[[proxies]]
name = "api-routes"
type = "http"
localPort = 8080
customDomains = ["example.com"]
locations = ["/api/v1", "/api/v2"]

[[proxies]]
name = "frontend"
type = "http"
localPort = 3000
customDomains = ["example.com"]
locations = ["/"]

/api/* routes to port 8080; everything else routes to port 3000.


5.2 Proxy Type Quick Reference

Type Public Port Direction Use Case
tcp Yes (remotePort) Server → Local SSH, databases, raw TCP
udp Yes (remotePort) Server → Local DNS, games, VoIP
http Shared (vhostHTTPPort) Server → Local Web apps, virtual hosting
https Shared (vhostHTTPSPort) Server → Local TLS web apps
tcpmux Shared (tcpmuxHTTPConnectPort) Server → Local Multiple TCP on one port
stcp None Peer → Peer (via frps) Secure private TCP
xtcp None Peer → Peer (direct) P2P TCP, high throughput
sudp None Peer → Peer (via frps) Secure private UDP

5.3 Plugin Quick Reference

Plugin Primary Use Key Options
http_proxy HTTP/HTTPS forward proxy httpUser, httpPassword
socks5 SOCKS5 proxy username, password
static_file Serve local directory localPath, stripPrefix, httpUser, httpPassword
unix_domain_socket Expose UDS as TCP unixPath
http2https HTTP → HTTPS localAddr, hostHeaderRewrite, requestHeaders
https2http TLS termination localAddr, crtPath, keyPath, enableHTTP2
https2https TLS cert replacement localAddr, crtPath, keyPath, enableHTTP2
http2http HTTP header rewriting localAddr, hostHeaderRewrite, requestHeaders
tls2raw Strip TLS → raw TCP localAddr, crtPath, keyPath
virtual_net L3 virtual network (no extra options; requires feature gate)

6. Troubleshooting

6.1 Connection and Startup Errors


Error: dial tcp: connect: connection refused (frpc → frps)

Cause: frpc cannot reach frps on the configured serverAddr:serverPort.

Solution:

  1. Verify frps is running: systemctl status frps or check the frps process.
  2. Check the port is listening: ss -tlnp | grep 7000 (or netstat -an | grep 7000).
  3. Check firewall: ufw status or cloud security group rules — port 7000/tcp must be open inbound on the frps host.
  4. Confirm serverAddr and serverPort in frpc.toml are correct.

Prevention: Use ./frpc verify -c frpc.toml and ./frps verify -c frps.toml before starting.


Error: type [http] not supported when vhost http port is not set

Cause: frpc is registering an http-type proxy but vhostHTTPPort is not configured in frps.toml. (GitHub issue #5097)

Solution: Add to frps.toml:

vhostHTTPPort = 80

Restart frps. For HTTPS proxies, also add vhostHTTPSPort = 443.

Prevention: Always configure vhostHTTPPort on frps when any client uses type = "http".


Error: login to server failed: auth failed or authorization failed

Cause: Token mismatch between frpc and frps.

Solution:

  1. Compare [auth] token values in both frps.toml and frpc.toml — they must be identical, including whitespace and case.
  2. If using tokenSource, verify the file or command returns the exact expected value.
  3. If using OIDC, confirm the token endpoint is reachable and credentials are correct.

Error: port already in use / bind: address already in use

Cause: Another process is already using remotePort or bindPort.

Solution:

  1. Find the conflicting process: ss -tlnp | grep :PORT or lsof -i :PORT.
  2. Either stop the conflicting process or choose a different remotePort.
  3. Check allowPorts on frps — if set, the requested port may be outside the allowed range.

Error: proxy name already exists

Cause: Two frpc instances are registering a proxy with the same name for the same authenticated user.

Solution: Ensure every proxy name is unique across all frpc instances connecting to the same frps.


Error: transport: authentication handshake failed: tls: certificate verify failed

Cause: TLS certificate mismatch. The client's trustedCaFile doesn't trust the server's certificate, or serverName doesn't match the cert's CN/SAN.

Solution:

  1. Set transport.tls.trustedCaFile to the CA that signed the server cert.
  2. Set transport.tls.serverName to match the certificate's Common Name or a Subject Alternative Name.

Error: transport.tls.force = false not working as expected

Cause: Known behavior issue (GitHub issue #5131). Mixed TLS/non-TLS behavior on frps does not always work as expected.

Solution: Be explicit — either force TLS on all connections (force = true on server + tls.enable = true on all clients), or explicitly disable TLS everywhere. Do not mix modes.


Error: failed to get workspace, maybe already shutdown

Cause: frps process has stopped or the connection was lost before the proxy registered.

Solution: Ensure frps is running and network connectivity is stable. Review frps logs for crash or shutdown reasons.


Error: Docker container build fails

Cause: Known issue with the Docker build configuration (GitHub issue #5288).

Solution: Build from source on the host (make) or use pre-built binaries. Alternatively, use the official Docker Hub images fatedier/frps and fatedier/frpc.


Error: EOF from frpc during startup

Cause: frps closed the connection immediately — usually due to auth failure or version mismatch.

Solution:

  1. Check frps logs for more detail.
  2. Verify tokens match on both sides.
  3. Ensure both frps and frpc are the same version (./frps -v and ./frpc -v).

Error: [W] [control.go] login from client error: [...] max ports per client

Cause: The frps maxPortsPerClient limit has been reached.

Solution: Reduce the number of proxies on the client, or increase maxPortsPerClient in frps.toml (0 = unlimited).


Error: Config parse error on startup

Cause: TOML syntax error or unrecognized config key.

Solution:

./frps verify -c frps.toml
./frpc verify -c frpc.toml

Use --strict_config for maximum validation. Check for typos in key names — TOML keys are camelCase in frp (e.g., serverAddr, not server_addr).


6.2 HTTP and HTTPS Proxy Issues


Error: 404 Not Found from frps for HTTP proxies

Cause: The Host header in the request doesn't match any registered customDomains or subdomain.

Solution:

  1. Confirm DNS for the domain points to the frps server's IP.
  2. Check that customDomains in frpc.toml matches the requested domain exactly (including www. prefix if applicable).
  3. If using subdomains, confirm subdomainHost is set in frps.toml and the subdomain matches.

Error: HTTP proxy returns 502 Bad Gateway

Cause: frpc registered successfully but cannot reach the local service at localIP:localPort.

Solution:

  1. Verify the local service is running: curl http://127.0.0.1:localPort/.
  2. Check localIP — in Docker, 127.0.0.1 inside the frpc container is the container itself, not the Docker host. Use the host's container IP or --network host.
  3. If a health check is configured with maxFailed = 1 and the app is slow to start, the proxy may be removed before the app is ready.

Error: HSTS / mixed-content browser errors with HTTPS proxies

Cause: Protocol mismatch between what the browser expects and what frps delivers.

Solution:

  1. Use the https2http plugin on frpc with a real certificate to terminate TLS properly.
  2. Ensure customDomains matches the exact domain in the certificate.
  3. For Let's Encrypt: provision and renew certs on the machine running frpc, not frps.

Error: HTTP basic auth prompt loops or always fails

Cause: httpUser/httpPassword in the proxy config does not match what the browser is sending, or credentials are cached incorrectly.

Solution:

  1. Verify credentials are correct in frpc.toml.
  2. Clear browser saved credentials and try again.
  3. Test with curl to rule out browser caching: curl -u user:pass http://domain/.

Error: Request headers not being set on forwarded requests

Cause: requestHeaders.set may have wrong syntax in TOML.

Solution: Ensure headers are under the correct section:

[[proxies]]
name = "web"
type = "http"
localPort = 8080
customDomains = ["example.com"]
[proxies.requestHeaders.set]
  X-Custom = "value"

6.3 UDP Issues


Error: UDP proxy drops large packets

Cause: udpPacketSize defaults to 1500 (Ethernet MTU). Packets larger than this are silently dropped (GitHub issue #5289).

Solution: Increase udpPacketSize in both frpc.toml and frps.toml:

udpPacketSize = 4096

Set it to match your application's maximum datagram size.


Error: UDP tunnel intermittently loses data

Cause: UDP is inherently unreliable; frp does not add reliability guarantees at the tunnel level.

Solution: For applications requiring reliability over UDP (e.g., QUIC-based apps), consider whether the application's own retry logic is sufficient, or use a TCP-based approach if possible.


6.4 Secret and P2P Proxy Issues


Error: stcp visitor can't connect — server not found

Cause: serverName in the visitor config doesn't match the name of the proxy.

Solution: serverName in [[visitors]] must exactly match name in [[proxies]]. These are case-sensitive.


Error: xtcp NAT hole punching fails — no direct connection established

Cause: Symmetric NAT on one or both sides prevents hole punching. The STUN server may also be unreachable.

Solution:

  1. Verify natHoleStunServer is reachable from both frpc instances: nc -u stun.easyvoip.com 3478.
  2. Configure fallbackTo pointing to an stcp visitor as fallback.
  3. If behind symmetric NAT on both sides, xtcp will not work — use stcp instead.

Error: xtcp throughput lower than expected

Cause: Known limitation for some NAT/routing configurations (GitHub issue #5281). The direct P2P path may be suboptimal.

Solution: Compare performance with stcp for your specific network setup. Use stcp if throughput is a priority.


Error: stcp allowUsers is rejecting a valid visitor

Cause: The visitor's frpc login name doesn't match an entry in allowUsers.

Solution: Check the frpc login name (derived from authentication method and client identity). Use allowUsers = ["*"] to permit all authenticated visitors during debugging, then tighten.


6.5 Performance and Stability


Error: Traffic metrics not updating in real time on dashboard

Cause: Known display issue — some proxy types don't update traffic counters in real time (GitHub issue #5282).

Solution: Refresh the dashboard page. Metrics are eventually consistent. Actual traffic forwarding is unaffected.


Error: High CPU usage on frpc

Cause: Excessive transport.poolCount, frequent proxy open/close cycles, or many active health checks.

Solution:

  1. Reduce transport.poolCount (default is 1 — increase only when needed).
  2. Increase intervalSeconds for health checks if sub-second precision isn't required.

Error: Connection drops after idle period

Cause: Firewall or NAT device timed out the TCP connection before frp's keepalive fired.

Solution:

  1. With transport.tcpMux = true (default), lower tcpMuxKeepaliveInterval:
[transport]
tcpMuxKeepaliveInterval = 15
  1. If tcpMux = false, set heartbeatInterval = 30 and heartbeatTimeout = 90 in frpc.
  2. Switch to protocol = "kcp" which handles keepalives differently.

Error: frpc reconnects in a loop at startup

Cause: loginFailExit = true (default) causes frpc to exit on auth failure; systemd's Restart=on-failure restarts it immediately, creating a loop.

Solution:

  1. Check actual error in frpc logs — it's usually an auth failure or network issue.
  2. To allow frpc to retry indefinitely: loginFailExit = false.

Error: QUIC connection fails or keeps dropping

Cause: ECN-marked packets being dropped by intermediate routers or NAT devices.

Solution: Disable ECN:

export QUIC_GO_DISABLE_ECN=1
./frpc -c frpc.toml

Or in the systemd unit:

[Service]
Environment=QUIC_GO_DISABLE_ECN=1

6.6 SSH Tunnel Gateway Issues


Error: Permission denied (publickey) when using SSH tunnel gateway

Cause: The SSH public key is not in the authorizedKeysFile, or it's in an unrecognized format.

Solution:

  1. Add your public key to the authorizedKeysFile path configured in frps.
  2. Verify format: one ssh-rsa or ssh-ed25519 entry per line, same format as ~/.ssh/authorized_keys.
  3. Validate key: ssh-keygen -l -f ~/.ssh/id_ed25519.pub.

Error: SSH tunnel exits immediately with no output

Cause: The requested remote port is outside allowPorts on frps, or the requested proxy type is unsupported.

Solution:

  1. Check allowPorts in frps.toml.
  2. Confirm the proxy type is supported by the gateway: tcp, http, https, tcpmux, stcp, sudp.

Error: SSH gateway host key mismatch warning

Cause: The frps SSH gateway host key changed (e.g., frps was reinstalled or the key was regenerated).

Solution: Remove the old entry from ~/.ssh/known_hosts:

ssh-keygen -R [frps-server]:2200

Then reconnect and accept the new host key.


6.7 Config Reload Issues


Error: Config reload (/api/reload) has no effect for some changes

Cause: Server address, transport protocol, and authentication method cannot be hot-reloaded — they require a full restart.

Solution: Proxy-level additions, removals, and modifications can be reloaded. Restart frpc for transport or auth changes.


7. FAQ

Q: What is frp and what problem does it solve? frp is a reverse proxy that makes local services accessible from the public internet when your machine is behind NAT or a firewall. frpc (client) establishes an outbound connection to frps (server on a public host), and incoming traffic to frps is relayed through that tunnel to your local service. No inbound port forwarding on your router is needed.


Q: What is the difference between frps and frpc? frps is the server — it runs on a public host with a reachable IP, listens for client connections and end-user traffic, and routes them together. frpc is the client — it runs on the machine behind NAT, connects outbound to frps, and declares which local services to expose.


Q: Do I need to open any ports on my local machine or router? No. frpc makes an outbound connection to frps. No inbound ports need to be opened on the local machine or its NAT router. Only the frps host needs ports open inbound (bindPort 7000 by default, plus any remotePort values you configure).


Q: Which config format should I use — TOML or INI? Use TOML. INI is the legacy format and is deprecated. New configs should be TOML. Existing INI configs should be migrated.


Q: Can I run multiple frpc instances connecting to the same frps? Yes. Multiple frpc instances can connect simultaneously. Each must use unique proxy names. If maxPortsPerClient is set, it applies independently per connected client.


Q: Can multiple proxies share the same remote port? For tcp and udp type proxies, each proxy needs a unique remotePort. For http/https, multiple proxies share vhostHTTPPort/vhostHTTPSPort because routing uses the HTTP Host header. For tcpmux, proxies share tcpmuxHTTPConnectPort.


Q: What is stcp and when should I use it instead of tcp? stcp exposes a service to specific trusted clients without opening any public port on frps. Both sides need a matching secretKey. Use stcp for databases, internal tools, or anything that shouldn't be publicly accessible. Use tcp only when the service is genuinely meant to be public.


Q: What is xtcp and when should I use it? xtcp attempts a direct peer-to-peer connection via NAT hole punching — frps is only used for signaling. Once connected, traffic flows directly between the two frpc clients without passing through frps. Use it for high-throughput or latency-sensitive scenarios between two clients behind NAT. Note: it requires a reachable STUN server and doesn't work through symmetric NAT. Always configure a fallbackTo stcp visitor.


Q: What is a visitor in frp? A visitor is a [[visitors]] block in frpc that allows that client to connect to an stcp, xtcp, or sudp proxy registered by another frpc instance. The visitor binds a local port; frp securely routes connections on that port to the remote service.


Q: How do I check if a proxy registered successfully?

# Via frpc admin API
curl http://admin:admin@127.0.0.1:7400/api/status | jq .

# Or watch frpc logs for:
# [proxy_name] start proxy success

Q: How do I reload frpc config without restarting the process?

curl http://admin:admin@127.0.0.1:7400/api/reload

This applies changes to [[proxies]] and [[visitors]]. Transport and server connection settings require a full restart.


Q: What is transport.poolCount and should I change it? poolCount (default: 1) controls how many pre-created connections frpc maintains to frps, reducing latency when new proxy connections are established. Increase it (e.g., 5) if you have many proxies opening/closing frequently and observe connection latency. For steady, long-lived proxies, the default of 1 is fine.


Q: What does loginFailExit do? When loginFailExit = true (default), frpc exits immediately if the initial connection or authentication to frps fails. Set it to false to make frpc retry indefinitely — useful when frps may not be running yet at frpc startup.


Q: Can frp handle multiple domains for one proxy? Yes. customDomains is a list:

[[proxies]]
name = "multi-domain"
type = "http"
localPort = 8080
customDomains = ["example.com", "www.example.com", "alt.example.com"]

Q: What is subdomainHost and how does it differ from customDomains? subdomainHost is a wildcard parent domain set once on frps (e.g., frp.example.com). Clients set subdomain = "myapp" to get myapp.frp.example.com. You only need one DNS wildcard record (*.frp.example.com → frps IP). customDomains requires each domain individually DNS-pointed to frps, but works for any domain name — not just subdomains of a parent.


Q: How does HTTP path routing work with locations?

[[proxies]]
name = "api-only"
type = "http"
localPort = 8080
customDomains = ["example.com"]
locations = ["/api", "/v2"]

Only requests whose paths begin with /api or /v2 are routed to this proxy. Other paths on the same domain can route to a different proxy.


Q: How do I add custom request or response headers?

[[proxies]]
name = "web"
type = "http"
localPort = 8080
customDomains = ["app.example.com"]
[proxies.requestHeaders.set]
  X-Real-IP = "$remote_addr"
  X-Forwarded-Proto = "https"
[proxies.responseHeaders.set]
  Cache-Control = "no-store"

Q: Can I use frp with a wildcard TLS certificate? Yes. Configure the https2http plugin on frpc with your wildcard cert (e.g., *.example.com), and use any matching subdomain in customDomains.


Q: What is routeByHTTPUser and how does it work? When routeByHTTPUser = true, frps uses the HTTP basic auth username to decide which proxy handles the request. Different usernames can be routed to different backends on the same domain and path. This requires HTTP basic auth to be present on the request.


Q: How do I expose a service that only listens on a Unix domain socket?

[[proxies]]
name = "uds-service"
type = "tcp"
remotePort = 9000
[proxies.plugin]
type = "unix_domain_socket"
unixPath = "/var/run/myapp.sock"

Q: What does hostHeaderRewrite do? It replaces the Host header sent to the local application. Useful when the local app expects a specific hostname different from the public domain. For example, if the public domain is app.example.com but the local app only responds to localhost:

hostHeaderRewrite = "localhost"

Q: Does frp support IPv6? Yes. Set bindAddr and localIP to IPv6 addresses:

# frps.toml
bindAddr = "::"

# frpc.toml
serverAddr = "2001:db8::1"

Q: Can I use frp without any authentication? Technically yes — omitting [auth] requires no token. This is acceptable only on private, trusted networks. For any public-facing frps, always set auth.token.


Q: What environment variables does frp respect?

Variable Effect
http_proxy Used as transport.proxyURL if not set in config
QUIC_GO_DISABLE_ECN Disable ECN marking for QUIC transport

Q: Can frp be used as an outbound HTTP/HTTPS proxy for browsing? Yes. Use the http_proxy or socks5 plugin. Configure your browser or system proxy to point to frps-server:remotePort. All HTTP/HTTPS traffic routes through the machine running frpc.


Q: Where is full configuration reference and architecture documentation? This guide covers user-facing usage. See the Admin Guide for complete configuration reference, architecture internals, deployment, security hardening, monitoring, and operational procedures.


Q: Does frpc reconnect automatically if the connection to frps drops? Yes. frpc automatically reconnects when the connection is lost. The loginFailExit setting only affects the very first connection attempt.


Q: How do I know if xtcp NAT hole punching succeeded? Check frpc logs on the visitor side:

  • hole punch successful — P2P connection is active, traffic flows directly.
  • falling back to [visitor-name] — P2P failed, using the configured stcp fallback.
  • If no fallback is configured and hole punching fails, the connection will not be established.

Q: Can I run frps behind a load balancer? Yes, with caveats. For raw TCP/UDP proxies, frps must be the public endpoint (or use a TCP pass-through load balancer). For HTTP proxies, frps can sit behind nginx or HAProxy if vhostHTTPPort traffic is forwarded. See the Admin Guide for deployment topology details.


Q: How do I migrate from INI to TOML config? frp does not provide an automatic migration tool. Key differences:

# Old INI frpc.ini
[common]
server_addr = x.x.x.x
server_port = 7000
token = secret

[ssh]
type = tcp
local_port = 22
remote_port = 6000
# New TOML frpc.toml
serverAddr = "x.x.x.x"
serverPort = 7000
[auth]
token = "secret"

[[proxies]]
name = "ssh"
type = "tcp"
localPort = 22
remotePort = 6000

Key changes: snake_case → camelCase, [common] → top-level keys, each [section] proxy → [[proxies]] with explicit name = "...".


Q: How do I debug frp connection problems step by step?

  1. Validate configs: ./frps verify -c frps.toml and ./frpc verify -c frpc.toml.
  2. Set both to log.level = "debug" temporarily.
  3. Start frps first, watch logs.
  4. Start frpc, watch logs for the login handshake.
  5. Test the proxy from the public side: curl or telnet frps-server remotePort.
  6. Check both sides simultaneously using tail -f on log files.
  7. For TLS issues, use openssl s_client -connect frps-server:7000 to inspect the certificate chain.

Q: What is the --allow-unsafe flag? It permits certain configuration options considered unsafe for production (such as configs that expose overly broad access). Do not use this in production environments.


Q: Does frp support HTTP/2 to the backend? Yes, when using the https2http or https2https plugins with enableHTTP2 = true. The control channel between frpc and frps uses its own multiplexing (tcpMux), independent of HTTP/2 to backends.


Q: Can two frpc instances run on the same machine and both connect to frps? Yes. Each can use the same token for auth but must have unique proxy names. They operate independently.