frp is a high-performance reverse proxy built for exposing local servers behind NAT or firewalls to the public internet. It ships two binaries — frps (the server, runs on a public host) and frpc (the client, runs behind NAT) — and supports TCP, UDP, HTTP, HTTPS, STCP, XTCP, SUDP, and TCPMux proxy types. Written in Go with a TOML-first config model. Licensed Apache 2.0.
Important
TOML is the preferred configuration format as of current versions. The legacy INI format is deprecated and will be removed in a future release. Migrate any INI configs to TOML immediately.
1. Architecture Overview
1.1 High-Level Design
frp operates on a persistent control channel model. The client (frpc) establishes a long-lived TCP (or KCP/QUIC/WebSocket) connection to the server (frps). Over this control channel, both sides exchange structured protocol messages to coordinate proxy creation, work connection dispatch, and heartbeating. No inbound connectivity is required on the client side — all initiation flows from server to client over the pre-established channel.
┌──────────────────────────────────────────────────────────────────────┐
│ Public Internet │
│ │
│ External User ──→ frps (public IP) ──→ frpc ──→ Local Service │
│ │ │
│ └─ Dashboard (web UI + API) │
└──────────────────────────────────────────────────────────────────────┘
1.2 Binary Entrypoints
| Binary | Entrypoint | Description |
|---|---|---|
frps |
cmd/frps/main.go |
Server process. Runs on the public-facing host. Binds ports, manages client sessions, proxies inbound traffic to clients. CLI via Cobra: flags -c (config path), -v (version), --strict_config, --allow-unsafe. |
frpc |
cmd/frpc/main.go → cmd/frpc/sub/ |
Client process. Runs behind NAT. Connects outbound to frps. Sub-commands live in cmd/frpc/sub/ (verify, nathole discover, nathole testing, etc.). |
1.3 Server-Side Component Map
| Component | Source | Role |
|---|---|---|
server/service.go |
Service struct |
Root server service. Initializes all sub-systems, starts listeners, manages lifecycle. |
server/control.go |
Control struct |
Per-client session manager. Handles message dispatch, proxy registration, heartbeat tracking, work connection pool management. One Control per connected frpc. |
server/proxy/ |
Proxy type implementations | TCP, UDP, HTTP, HTTPS, STCP, XTCP, SUDP, TCPMux proxy types. Each type implements Proxy interface with Run() and Close(). |
server/group/ |
Group managers | Load balancing across multiple frpc clients for HTTP, HTTPS, TCP, and TCPMux proxy types. |
server/registry/ |
ClientRegistry |
Tracks all connected clients. Key–value store for client lookup. |
server/visitor/ |
Visitor manager | Manages visitor-side connections for STCP/XTCP peer-to-peer tunnels. |
server/http/controller.go |
Dashboard API | REST controller backing the web dashboard. Exposes proxy stats, traffic metrics, client list. |
server/ports/ports.go |
Port manager | Allocates and releases ports for TCP/UDP proxies. Enforces allowPorts policy. |
1.4 Client-Side Component Map
| Component | Source | Role |
|---|---|---|
client/service.go |
Service struct |
Root client service. Reads config, manages lifecycle, spawns Control. |
client/control.go |
Control struct |
Manages proxy managers and visitor managers. Processes server messages. Drives the proxy lifecycle. |
client/connector.go |
Connector |
Handles the physical connection to frps. Supports TCP, KCP, QUIC, WebSocket, WSS transports with optional TLS. Reconnects on failure. |
client/proxy/ |
Proxy managers | Client-side proxy logic. Receives work connection requests from server, connects to local service, bridges traffic. |
client/visitor/ |
Visitor implementations | STCP, SUDP, XTCP visitors. XTCP uses KCP or QUIC tunnel sessions for hole-punched traffic. |
client/http/controller.go |
Admin API | REST controller for frpc admin interface. Config reload, status, proxy/visitor CRUD. |
client/config_manager.go |
Config manager | Loads, validates, and hot-manages proxy/visitor configs. Implements Store API. |
1.5 Shared Packages
| Package | Source | Role |
|---|---|---|
pkg/config/ |
Config loading | TOML/YAML/JSON/INI parsing, template rendering, flag registration, config source abstraction. |
pkg/config/v1/ |
Config structs | ServerConfig, ClientCommonConfig, all proxy and visitor config types, plugin configs. |
pkg/config/v1/validation/ |
Validators | Validates server and client configs; returns structured errors. |
pkg/config/source/ |
Source abstraction | ConfigSource, StoreSource, Aggregator — allows composing config from file + dynamic store. |
pkg/auth/ |
Authentication | Token auth and OIDC auth (with token caching and retry). |
pkg/transport/ |
Transport layer | TLS config builder, MessageTransporter (framed message R/W over any io.ReadWriteCloser). |
pkg/msg/ |
Protocol messages | All wire-format message types. Uses JSON encoding. |
pkg/nathole/ |
NAT traversal | STUN-based NAT type discovery, NAT classification, hole-punch coordination, analysis data. |
pkg/ssh/ |
SSH gateway | SSH tunnel gateway for frps; allows ssh -R style tunneling through frp. |
pkg/vnet/ |
Virtual network | Experimental TUN-based virtual network using WireGuard/netlink. Feature-gated. |
pkg/plugin/client/ |
Client plugins | 10 built-in client-side plugins (http_proxy, socks5, static_file, unix_domain_socket, etc.). |
pkg/plugin/server/ |
Server plugins | HTTP plugin system. frps POSTs operation events to external HTTP endpoints. |
pkg/metrics/ |
Metrics | In-memory counters, Prometheus exporter, aggregate metrics. |
pkg/virtual/ |
Virtual client | Used by SSH tunnel gateway to inject virtual frpc connections into the server. |
1.6 Data Flow — Connection Lifecycle
1. frpc starts → Connector dials frps (TCP/KCP/QUIC/WS/WSS, optional TLS)
2. frpc sends Login{version, hostname, os, arch, user, auth_token, pool_count, client_id}
3. frps validates auth → registers Control in ControlManager + ClientRegistry
4. frps sends LoginResp{run_id, server_udp_port}
5. frpc sends NewProxy{proxy_name, proxy_type, ...config} for each proxy
6. frps validates proxy config → creates proxy instance → binds port/vhost
7. frps sends NewProxyResp{proxy_name, remote_addr, error}
8. External user connects to frps on proxy port/vhost
9. frps sends ReqWorkConn to frpc
10. frpc dials frps work conn port → sends StartWorkConn{proxy_name}
11. frps bridges: external conn ↔ work conn
12. frpc bridges: work conn ↔ local service
13. frpc sends Ping (heartbeat) → frps responds Pong
14. On disconnect: frps closes proxies, removes Control
1.7 Web Frontend
| Frontend | Source | Stack |
|---|---|---|
| frps dashboard | web/frps/ |
Vue 3 + Element Plus + Vite |
| frpc admin UI | web/frpc/ |
Vue 3 + Element Plus + Pinia + Vite |
2. System Requirements
2.1 Runtime Requirements
| Requirement | frps | frpc |
|---|---|---|
| OS | Linux, macOS, Windows, FreeBSD | Linux, macOS, Windows, FreeBSD |
| Architecture | amd64, arm64, arm, 386, mips | amd64, arm64, arm, 386, mips |
| Network | Public IP or port-forwarded host | Outbound TCP to frps port (default 7000) |
| Firewall | Open bindPort (7000), vhostHTTPPort (80), vhostHTTPSPort (443), dashboard port | No inbound ports required |
| Memory | ~30 MB base + ~2 MB/connected client | ~20 MB base |
| Disk | Minimal (log files only) | Minimal |
2.2 Build Requirements
| Tool | Version | Purpose |
|---|---|---|
| Go | 1.25+ | Build both binaries |
| Node.js | 18+ | Build web dashboards |
| make | Any | Makefile targets |
# Build all
make build
# Build server only
go build -o frps ./cmd/frps
# Build client only
go build -o frpc ./cmd/frpc
# Build web assets (required for embedded dashboard)
cd web/frps && npm install && npm run build
cd web/frpc && npm install && npm run build
2.3 Docker Images
Pre-built Docker images are available. Custom Dockerfiles live in dockerfiles/:
| File | Target |
|---|---|
dockerfiles/Dockerfile-for-frpc |
frpc container image |
dockerfiles/Dockerfile-for-frps |
frps container image |
# Build frps image
docker build -f dockerfiles/Dockerfile-for-frps -t frps:local .
# Build frpc image
docker build -f dockerfiles/Dockerfile-for-frpc -t frpc:local .
Note
GitHub issue #5288 documents Docker build failures on certain base images. Ensure the Go toolchain version matches what is specified in
go.mod.
3. Configuration Reference
3.1 Config File Format
frp supports TOML (preferred), YAML, JSON, and INI (deprecated). The config format is auto-detected from the file extension. Template rendering is supported: {{ .Envs.VARIABLE }} expands environment variables in config values.
# Start with config file
frps -c /etc/frp/frps.toml
frpc -c /etc/frp/frpc.toml
# Strict config (fail on unknown keys)
frps -c frps.toml --strict_config
# Allow unsafe config options
frps -c frps.toml --allow-unsafe
3.2 Server Configuration (frps.toml) — Complete Reference
All fields from ServerConfig struct in pkg/config/v1/server.go:
Bind Settings
bindAddr = "0.0.0.0" # IP to bind control port (default: "0.0.0.0")
bindPort = 7000 # Control plane TCP port (default: 7000)
kcpBindPort = 7000 # KCP UDP port (0 = disabled)
quicBindPort = 7002 # QUIC UDP port (0 = disabled)
proxyBindAddr = "0.0.0.0" # IP for proxy listener binds (default: same as bindAddr)
Virtual Host Settings
vhostHTTPPort = 80 # HTTP vhost reverse proxy port (0 = disabled)
vhostHTTPSPort = 443 # HTTPS vhost reverse proxy port (0 = disabled)
vhostHTTPTimeout = 60 # Backend HTTP response timeout in seconds (default: 60)
tcpmuxHTTPConnectPort = 0 # TCPMux HTTP CONNECT port (0 = disabled)
tcpmuxPassthrough = false # Pass CONNECT request to backend unchanged
subDomainHost = "" # Base domain for subdomain-based HTTP proxies (e.g. "example.com")
Web Dashboard / Admin API
[webServer]
addr = "127.0.0.1" # Dashboard bind address
port = 7500 # Dashboard port
user = "admin" # HTTP basic auth username
password = "secret" # HTTP basic auth password
assetsDir = "" # Override embedded assets with filesystem path
pprofEnable = false # Enable pprof at /debug/pprof/
[webServer.tls]
certFile = ""
keyFile = ""
Metrics
enablePrometheus = false # Expose /metrics endpoint (Prometheus format)
Logging
[log]
to = "console" # Log destination: "console", or a file path
level = "info" # Log level: trace, debug, info, warn, error
maxDays = 3 # Days to keep log files (0 = no rotation)
disablePrintColor = false # Disable ANSI color in console output
Transport
[transport]
maxPoolCount = 5 # Max pre-allocated work connections per client (default: 5)
tcpMux = true # Enable yamux TCP multiplexing (default: true)
tcpMuxKeepaliveInterval = 60 # Yamux keepalive interval seconds
tcpKeepalive = 7200 # TCP keepalive interval seconds (-1 = disable)
heartbeatTimeout = 90 # Client heartbeat timeout seconds (default: 90, -1 = disable)
[transport.tls]
force = false # Require TLS for all client connections
certFile = "" # TLS certificate file path
keyFile = "" # TLS private key file path
trustedCaFile = "" # CA file for mutual TLS client verification
[transport.quic]
keepalivePeriod = 10 # QUIC keepalive period seconds
maxIdleTimeout = 30 # QUIC max idle timeout seconds
maxIncomingStreams = 100000 # QUIC max concurrent streams
Warning
GitHub issue #5131 documents that
transport.tls.force = falsemay not behave as expected in all configurations. Test TLS behavior explicitly in your environment before production deployment.
Authentication
[auth]
method = "token" # Auth method: "token" or "oidc"
additionalScopes = [] # Additional auth scopes: ["NewProxy", "NewWorkConn"]
# Token auth
token = "your-secret-token"
# OIDC auth
[auth.oidc]
issuer = "" # OIDC issuer URL
audience = "" # Expected audience claim
skipExpiryCheck = false
skipIssuerCheck = false
Access Control
detailedErrorsToClient = true # Send detailed error messages to frpc (default: true)
userConnTimeout = 10 # Seconds to wait for user connection to work conn (default: 10)
maxPortsPerClient = 0 # Max ports per client (0 = unlimited)
custom404Page = "" # Path to custom 404 HTML page for vhost proxies
# Port allowlist (restricts TCP/UDP proxy ports)
[[allowPorts]]
start = 2000
end = 3000
[[allowPorts]]
start = 4000
end = 4100
[[allowPorts]]
single = 7000 # Single port allowance
UDP
udpPacketSize = 1500 # Max UDP packet size bytes (default: 1500; match frpc setting)
NAT Hole Analysis
natholeAnalysisDataReserveHours = 168 # Hours to retain NAT analysis data (default: 168 = 7 days)
SSH Tunnel Gateway
[sshTunnelGateway]
bindPort = 2222 # SSH gateway listen port (0 = disabled)
privateKeyFile = "" # Server private key path
autoGenPrivateKeyPath = "" # Auto-generate key and save to this path
authorizedKeysFile = "" # Authorized public keys file (OpenSSH format)
HTTP Plugins
[[httpPlugins]]
name = "my-plugin"
addr = "127.0.0.1:9000" # Plugin HTTP server address
path = "/handler" # Plugin endpoint path
ops = ["Login", "NewProxy"] # Operations to forward to plugin
tlsVerify = false # Verify plugin TLS certificate
3.3 Client Configuration (frpc.toml) — Complete Reference
All fields from ClientCommonConfig in pkg/config/v1/client.go:
Server Connection
serverAddr = "0.0.0.0" # frps host/IP (default: "0.0.0.0")
serverPort = 7000 # frps control port (default: 7000)
natHoleStunServer = "stun.easyvoip.com:3478" # STUN server for XTCP
dnsServer = "" # Override DNS server (e.g. "8.8.8.8")
Identity
user = "" # User prefix for proxy names (namespacing)
clientID = "" # Client ID (auto-generated if empty)
Auth (same structure as server)
[auth]
method = "token"
token = "your-secret-token"
Behavior
loginFailExit = true # Exit if login fails (default: true); false = keep retrying
start = [] # Only start these proxy names (empty = all)
Logging (same structure as server)
[log]
to = "console"
level = "info"
maxDays = 3
disablePrintColor = false
Admin Web Interface
[webServer]
addr = "127.0.0.1"
port = 7400
user = "admin"
password = "secret"
pprofEnable = false
Transport
[transport]
protocol = "tcp" # "tcp", "kcp", "quic", "websocket", "wss"
dialServerTimeout = 10 # Seconds to wait for initial connection
dialServerKeepalive = 7200 # TCP keepalive seconds
connectServerLocalIP = "" # Bind local IP for connection to server
proxyURL = "" # HTTP/SOCKS5 proxy for connecting to server
poolCount = 1 # Pre-allocated work connections
heartbeatInterval = 30 # Heartbeat send interval seconds
heartbeatTimeout = 90 # Heartbeat timeout seconds
tcpMux = true
tcpMuxKeepaliveInterval = 60
tls.enable = false
tls.disableCustomTLSFirstByte = false
tls.certFile = ""
tls.keyFile = ""
tls.trustedCaFile = ""
tls.serverName = ""
[transport.quic]
keepalivePeriod = 10
maxIdleTimeout = 30
maxIncomingStreams = 100000
Virtual Network (Experimental)
[virtualNet]
address = "" # TUN interface CIDR (e.g. "10.0.0.1/24")
Feature Gates
[featureGates]
VirtualNet = false # Enable experimental vnet (TUN device)
Other
udpPacketSize = 1500
metadatas = {key = "value"} # Arbitrary metadata sent to server/plugins
includes = ["./proxies/*.toml"] # Include additional config files
Store (Dynamic Config)
[store]
enabled = false # Enable dynamic proxy/visitor store
dataDir = "" # Directory for persisted store data
3.4 Proxy Type Configuration
TCP Proxy
[[proxies]]
name = "ssh"
type = "tcp"
localIP = "127.0.0.1"
localPort = 22
remotePort = 6000 # Port on frps to expose
[proxies.transport]
bandwidthLimit = "1MB" # e.g. "1MB", "1KB"
bandwidthLimitMode = "client" # "client" or "server"
useEncryption = false
useCompression = false
UDP Proxy
[[proxies]]
name = "dns"
type = "udp"
localIP = "127.0.0.1"
localPort = 53
remotePort = 6053
HTTP Proxy
[[proxies]]
name = "web"
type = "http"
localIP = "127.0.0.1"
localPort = 8080
customDomains = ["www.example.com"]
subdomain = "web" # Requires subDomainHost on server
locations = ["/"]
httpUser = "" # HTTP basic auth
httpPassword = ""
hostHeaderRewrite = "" # Rewrite Host header
requestHeaders.set = {X-From-Where = "frp"}
responseHeaders.set = {}
routeByHTTPUser = ""
Note
GitHub issue #5097: HTTP proxies require
vhostHTTPPortto be configured on frps. If not set, you will receive:type [http] not supported when vhost http port is not set.
HTTPS Proxy
[[proxies]]
name = "web-secure"
type = "https"
localIP = "127.0.0.1"
localPort = 8443
customDomains = ["www.example.com"]
subdomain = "web"
[proxies.plugin]
type = "https2http"
localAddr = "127.0.0.1:8080"
STCP (Secret TCP — peer-to-peer via frps relay)
# Proxy side (frpc that has the service)
[[proxies]]
name = "secret-ssh"
type = "stcp"
secretKey = "shared-secret"
localIP = "127.0.0.1"
localPort = 22
# Visitor side (frpc on the accessing machine)
[[visitors]]
name = "secret-ssh-visitor"
type = "stcp"
serverName = "secret-ssh"
secretKey = "shared-secret"
bindAddr = "127.0.0.1"
bindPort = 6000
XTCP (Peer-to-peer with NAT hole punching)
# Proxy side
[[proxies]]
name = "p2p-ssh"
type = "xtcp"
secretKey = "shared-secret"
localIP = "127.0.0.1"
localPort = 22
# Visitor side
[[visitors]]
name = "p2p-ssh-visitor"
type = "xtcp"
serverName = "p2p-ssh"
secretKey = "shared-secret"
bindAddr = "127.0.0.1"
bindPort = 6000
fallbackTo = "secret-ssh-visitor" # Fallback if hole punch fails
fallbackTimeoutMs = 200
Note
GitHub issue #5281: VirtualNet (XTCP) has lower throughput than standard XTCP. Use standard XTCP for performance-sensitive tunnels. The VirtualNet feature gate enables TUN-based routing but is experimental.
SUDP (Secret UDP)
[[proxies]]
name = "secret-udp"
type = "sudp"
secretKey = "shared-secret"
localIP = "127.0.0.1"
localPort = 9999
[[visitors]]
name = "secret-udp-visitor"
type = "sudp"
serverName = "secret-udp"
secretKey = "shared-secret"
bindAddr = "127.0.0.1"
bindPort = 9999
TCPMux (Multiple TCP streams on one connection via HTTP CONNECT)
[[proxies]]
name = "tcpmux"
type = "tcpmux"
multiplexer = "httpconnect"
localIP = "127.0.0.1"
localPort = 22
customDomains = ["tunnel.example.com"]
httpUser = ""
httpPassword = ""
routeByHTTPUser = ""
3.5 Client Plugin Configuration
Client-side plugins intercept local traffic before forwarding. 10 built-in plugins:
| Plugin | Type Key | Purpose |
|---|---|---|
| HTTP Proxy | http_proxy |
Local HTTP proxy server |
| SOCKS5 | socks5 |
Local SOCKS5 proxy server |
| Static File | static_file |
Serve local directory over HTTP |
| Unix Domain Socket | unix_domain_socket |
Forward to local UDS |
| HTTP2HTTPS | http2https |
Convert HTTP to HTTPS for local backend |
| HTTPS2HTTP | https2http |
Terminate TLS before forwarding |
| HTTPS2HTTPS | https2https |
TLS passthrough/conversion |
| HTTP Header Modifier | http_header |
Modify request/response headers |
| Tls2Raw | tls2raw |
Strip TLS before local backend |
[[proxies]]
name = "socks5-tunnel"
type = "tcp"
remotePort = 6005
[proxies.plugin]
type = "socks5"
username = "user"
password = "pass"
4. Deployment & Scaling
4.1 Bare Metal / VM Deployment
# Install frps as systemd service
sudo cp frps /usr/local/bin/
sudo mkdir -p /etc/frp
sudo cp frps.toml /etc/frp/
cat > /etc/systemd/system/frps.service << 'EOF'
[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=5s
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now frps
# Install frpc as systemd service
sudo cp frpc /usr/local/bin/
sudo mkdir -p /etc/frp
sudo cp frpc.toml /etc/frp/
cat > /etc/systemd/system/frpc.service << 'EOF'
[Unit]
Description=frp Client
After=network.target
[Service]
Type=simple
User=nobody
ExecStart=/usr/local/bin/frpc -c /etc/frp/frpc.toml
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now frpc
4.2 Docker Compose Deployment
# docker-compose.yml for frps
version: "3"
services:
frps:
image: fatedier/frps:latest
volumes:
- ./frps.toml:/etc/frp/frps.toml
ports:
- "7000:7000" # Control port
- "7500:7500" # Dashboard
- "80:80" # HTTP vhost
- "443:443" # HTTPS vhost
- "2000-3000:2000-3000" # Proxy port range
restart: unless-stopped
command: -c /etc/frp/frps.toml
# docker-compose.yml for frpc
version: "3"
services:
frpc:
image: fatedier/frpc:latest
network_mode: host # Use host network to reach local services
volumes:
- ./frpc.toml:/etc/frp/frpc.toml
restart: unless-stopped
command: -c /etc/frp/frpc.toml
4.3 Kubernetes Deployment
Deploy frps as a LoadBalancer or NodePort service. frpc runs as a DaemonSet or Deployment in the cluster with config via ConfigMap.
apiVersion: v1
kind: ConfigMap
metadata:
name: frps-config
data:
frps.toml: |
bindPort = 7000
[auth]
token = "changeme"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: frps
spec:
replicas: 1
selector:
matchLabels:
app: frps
template:
spec:
containers:
- name: frps
image: fatedier/frps:latest
args: ["-c", "/etc/frp/frps.toml"]
volumeMounts:
- name: config
mountPath: /etc/frp
volumes:
- name: config
configMap:
name: frps-config
Note
GitHub issue #4800 documents that frps clustering is a feature request and is not natively supported. For high-availability, use a single frps behind a TCP load balancer (HAProxy/nginx stream), or partition clients across multiple frps instances with different ports.
4.4 Scaling Considerations
| Concern | Guidance |
|---|---|
| Concurrent clients | Each Control goroutine is lightweight. Tested at thousands of concurrent clients. Monitor goroutine count via pprof. |
| Port ranges | Use allowPorts to pre-define the port range. Keep ranges narrow and document them. |
| Work connection pool | transport.maxPoolCount (server) and transport.poolCount (client) control pre-warmed connections. Increase for high-traffic proxies. |
| KCP vs TCP | KCP (kcpBindPort) is UDP-based and better for lossy networks. Has higher CPU overhead than TCP. |
| QUIC | quicBindPort provides multiplexing without head-of-line blocking. Recommended for high-concurrency scenarios over lossy links. |
| Bandwidth throttling | Per-proxy bandwidthLimit is enforced on the client side (bandwidthLimitMode = "client") or server side ("server"). Server-side requires --allow-unsafe flag on frps. |
5. Operations
5.1 frps API Endpoints
All dashboard API endpoints require HTTP Basic Auth (configured via webServer.user/webServer.password).
Caution
GitHub issue #5290 notes that
/healthzendpoints are accessible without dashboard authentication. Do not expose the dashboard port publicly without additional network-level controls.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /healthz |
None | Health check. Returns 200 if server is running. |
| GET | /metrics |
None | Prometheus metrics (requires enablePrometheus = true). |
| GET | /api/serverinfo |
Basic | Server version, Go version, arch, OS, bind info. |
| GET | /api/proxy/{type} |
Basic | List all proxies of type (tcp, udp, http, https, stcp, xtcp, sudp, tcpmux). |
| GET | /api/proxy/{type}/{name} |
Basic | Get proxy detail by type and name. |
| GET | /api/proxies/{name} |
Basic | Get proxy detail by name (type-agnostic). |
| GET | /api/traffic/{name} |
Basic | Traffic stats for a proxy (bytes in/out). |
| GET | /api/clients |
Basic | List all connected clients with metadata. |
| GET | /api/clients/{key} |
Basic | Get specific client by run ID. |
| DELETE | /api/proxies |
Basic | Force-close all proxies (use for cleanup). |
5.2 frpc Admin API Endpoints
frpc admin API requires Basic Auth configured via webServer.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /healthz |
None | Client health check. |
| GET | /api/reload |
Basic | Reload config file and apply changes. |
| POST | /api/stop |
Basic | Stop frpc process. |
| GET | /api/status |
Basic | Status of all proxies and visitors. |
| GET | /api/config |
Basic | Get current config. |
| PUT | /api/config |
Basic | Replace config (triggers reload). |
| GET | /api/proxy/{name}/config |
Basic | Get individual proxy config. |
| GET | /api/visitor/{name}/config |
Basic | Get individual visitor config. |
| GET | /api/store/proxies |
Basic | List dynamically stored proxies. |
| POST | /api/store/proxies |
Basic | Add a proxy to store. |
| GET | /api/store/proxies/{name} |
Basic | Get stored proxy config. |
| PUT | /api/store/proxies/{name} |
Basic | Update stored proxy config. |
| DELETE | /api/store/proxies/{name} |
Basic | Remove stored proxy. |
| GET | /api/store/visitors |
Basic | List dynamically stored visitors. |
| POST | /api/store/visitors |
Basic | Add visitor to store. |
| GET | /api/store/visitors/{name} |
Basic | Get stored visitor. |
| PUT | /api/store/visitors/{name} |
Basic | Update stored visitor. |
| DELETE | /api/store/visitors/{name} |
Basic | Remove stored visitor. |
| GET | /debug/pprof/ |
Basic | pprof index (requires pprofEnable = true). |
| GET | /debug/pprof/cmdline |
Basic | Command line. |
| GET | /debug/pprof/profile |
Basic | CPU profile (30s). |
| GET | /debug/pprof/symbol |
Basic | Symbol lookup. |
| GET | /debug/pprof/trace |
Basic | Execution trace. |
5.3 Hot Reload
Config changes can be applied without restarting frpc:
# Via API
curl -u admin:secret http://127.0.0.1:7400/api/reload
# Via frpc subcommand
frpc reload -c /etc/frp/frpc.toml
Note
GitHub issue #2946 (hot-reloading TLS certificates): TLS certificate hot-reload is a known limitation. Changing TLS certificates requires a full frps restart.
5.4 Monitoring
Prometheus metrics (requires enablePrometheus = true on frps):
frp_server_client_counts # Current connected client count
frp_server_proxy_counts{type} # Proxy count by type
frp_server_connection_counts # Current connection count
frp_server_traffic_in_bytes_total # Total inbound traffic bytes
frp_server_traffic_out_bytes_total # Total outbound traffic bytes
Note
GitHub issue #5282: Traffic metrics may not update in real-time under high-frequency connection scenarios. Use Prometheus scrape intervals of 15s or longer.
pprof profiling (requires pprofEnable = true):
# CPU profile (30 seconds)
curl http://127.0.0.1:7500/debug/pprof/profile?seconds=30 > cpu.prof
go tool pprof cpu.prof
# Goroutine dump
curl http://127.0.0.1:7500/debug/pprof/goroutine?debug=2
5.5 Log Analysis
Key log patterns to monitor:
| Pattern | Meaning |
|---|---|
login to server success |
frpc connected successfully |
proxy added: [name] |
Proxy successfully registered |
heartbeat timeout |
Client lost connectivity; will reconnect |
port already in use |
Requested remote port is occupied |
port not allowed |
Port outside allowPorts range |
exceed the max_ports_per_client |
Client hit port quota |
authentication failed |
Token/OIDC mismatch |
EOF on work connection |
Normal connection close |
i/o timeout |
Network timeout between frpc and local service |
6. Performance
6.1 Transport Selection
| Transport | Protocol | Use Case | Tradeoff |
|---|---|---|---|
| TCP | TCP | Default, all environments | Reliable; head-of-line blocking |
| KCP | UDP | Lossy/high-latency networks | Higher CPU; better throughput on packet loss |
| QUIC | UDP | Modern; multiplexed | Best for high-concurrency; requires UDP passthrough |
| WebSocket | TCP | Environments that block non-HTTP | HTTP overhead |
| WSS | TLS/TCP | WebSocket over TLS | HTTP overhead + TLS overhead |
6.2 Connection Pool Tuning
# frps
[transport]
maxPoolCount = 5 # Increase for high-traffic proxies (e.g. 20-50)
# frpc
[transport]
poolCount = 5 # Should match or exceed server maxPoolCount
Pre-warmed connections eliminate the latency of establishing new connections on each user request. Increase these values for proxies that handle many short-lived connections.
6.3 TCP Multiplexing
tcpMux = true (default) uses yamux to multiplex multiple streams over one TCP connection. This dramatically reduces the number of TCP connections needed for high-concurrency workloads. Disable only if debugging mux-specific issues.
6.4 Bandwidth Limiting
[[proxies]]
name = "limited-web"
type = "tcp"
[proxies.transport]
bandwidthLimit = "10MB"
bandwidthLimitMode = "server" # Enforced server-side (requires --allow-unsafe on frps)
6.5 UDP Packet Size
# Must match on both frps and frpc
udpPacketSize = 1500 # Increase for jumbo frames (e.g. 9000 on trusted LANs)
Warning
GitHub issue #5289: UDP proxies may not forward large packets reliably if
udpPacketSizeis smaller than the actual packet MTU. IncreaseudpPacketSizeto 4096 or higher for DNS-over-UDP and similar protocols that may exceed 1500 bytes.
7. Security
7.1 Authentication Methods
Token Authentication (Default)
# frps
[auth]
method = "token"
token = "use-a-long-random-secret-here"
# frpc
[auth]
method = "token"
token = "use-a-long-random-secret-here"
Token is transmitted in the Login message and compared server-side. Use a minimum 32-character random token in production.
OIDC Authentication
# frps
[auth]
method = "oidc"
[auth.oidc]
issuer = "https://accounts.google.com"
audience = "my-frp-service"
skipExpiryCheck = false
skipIssuerCheck = false
# frpc
[auth]
method = "oidc"
# Token source config for acquiring OIDC tokens goes here
OIDC tokens are cached and refreshed automatically in pkg/auth/. Scopes can be extended via additionalScopes to include NewProxy and NewWorkConn operations.
7.2 TLS Encryption
# frps — require TLS from all clients
[transport.tls]
force = true
certFile = "/etc/frp/server.crt"
keyFile = "/etc/frp/server.key"
trustedCaFile = "/etc/frp/ca.crt" # For mutual TLS
# frpc — connect with TLS
[transport]
protocol = "tcp"
[transport.tls]
enable = true
certFile = "/etc/frp/client.crt"
keyFile = "/etc/frp/client.key"
trustedCaFile = "/etc/frp/ca.crt"
serverName = "frps.example.com"
Important
Without TLS, all traffic between frps and frpc (including auth tokens and proxied data) is transmitted in plaintext. Always enable TLS in production deployments, especially when frps is reachable from the public internet.
7.3 Port Access Control
# Restrict which ports frpc can use
[[allowPorts]]
start = 2000
end = 3000
[[allowPorts]]
single = 443
maxPortsPerClient = 10 # Prevent port exhaustion by a single client
7.4 Per-Proxy HTTP Auth
[[proxies]]
name = "protected-web"
type = "http"
localPort = 8080
customDomains = ["app.example.com"]
httpUser = "admin"
httpPassword = "secret"
frps injects an HTTP Basic Auth challenge before forwarding to the backend. The backend itself does not need to implement auth.
7.5 Dashboard Security
- Always set
webServer.userandwebServer.password - Bind dashboard to
127.0.0.1(not0.0.0.0) unless remote access is required - Enable
webServer.tlsfor HTTPS dashboard - Consider firewall rules to restrict dashboard port access
Caution
The
/healthzendpoint on both frps and frpc dashboards is accessible without authentication (GitHub issue #5290). This exposes service uptime status. If your threat model requires hiding service existence, firewall the dashboard port entirely.
7.6 SSH Tunnel Gateway
The SSH tunnel gateway (sshTunnelGateway) allows standard ssh -R usage to create frp tunnels:
[sshTunnelGateway]
bindPort = 2222
autoGenPrivateKeyPath = "/etc/frp/ssh_host_key"
authorizedKeysFile = "/etc/frp/.authorized_keys"
# Client usage — expose local port 8080 as remote port 8080
ssh -p 2222 -R 8080:localhost:8080 [email removed]
The authorizedKeysFile controls which SSH public keys can create tunnels. The virtual client path goes through pkg/virtual/ and injects an in-process frpc-equivalent into the server.
7.7 Server Plugins for Custom Auth/Authz
[[httpPlugins]]
name = "auth-service"
addr = "http://127.0.0.1:9001"
path = "/frp/handler"
ops = ["Login", "NewProxy", "CloseProxy", "Ping", "NewWorkConn"]
tlsVerify = true
Plugins receive JSON POST requests for each operation. Returning HTTP 200 allows the operation; any other status rejects it. This enables full custom authorization logic (LDAP, database, ACL systems).
Plugin request format:
{
"version": "0.1.0",
"op": "Login",
"content": {
"version": "0.60.0",
"hostname": "client-host",
"os": "linux",
"arch": "amd64",
"user": "user1",
"timestamp": 1234567890,
"privilege_key": "...",
"run_id": "...",
"pool_count": 5,
"metas": {},
"client_address": "1.2.3.4:12345"
}
}
7.8 Network Security Recommendations
| Control | Recommendation |
|---|---|
| Firewall | Restrict bindPort to trusted source IPs if all clients are known |
| Reverse proxy | Put frps dashboard behind nginx/Traefik with additional auth |
| Secrets | Never commit tokens/TLS keys to version control |
| Audit | Enable file logging and ship to centralized log system |
| Monitoring | Alert on authentication failed spikes (brute force indicator) |
detailedErrorsToClient |
Set to false in production to limit information disclosure |
8. Developer Internals
8.1 Protocol Message Layer (pkg/msg/)
All communication between frps and frpc uses framed JSON messages over the control connection. Messages are length-prefixed. The MessageTransporter in pkg/transport/ handles framing.
| Message | Direction | Purpose |
|---|---|---|
Login |
C→S | Initial client handshake. Contains version, auth, client metadata, pool count. |
LoginResp |
S→C | Login result. Contains run_id (session ID), server_udp_port, error. |
NewProxy |
C→S | Register a new proxy. Contains all proxy config fields. |
NewProxyResp |
S→C | Proxy registration result. Contains remote_addr or error. |
CloseProxy |
C→S | Unregister a proxy. |
NewWorkConn |
C→S | Client opening a new work connection. |
ReqWorkConn |
S→C | Server requesting a work connection for a new user connection. |
StartWorkConn |
S→C | Server assigning work connection to a proxy/visitor. |
NewVisitorConn |
C→S | Visitor connecting to an STCP/XTCP proxy. |
NewVisitorConnResp |
S→C | Visitor connection result. |
Ping |
C→S | Heartbeat from client. Contains timestamp. |
Pong |
S→C | Heartbeat acknowledgment. |
UDPPacket |
both | UDP payload envelope for UDP proxy type. |
NatHoleVisitor |
C→S | Visitor initiating NAT hole punch. |
NatHoleClient |
C→S | Proxy side registering for NAT hole punch. |
NatHoleResp |
S→C | NAT hole punch response with peer address info. |
NatHoleSid |
both | Session ID for NAT hole punch coordination. |
NatHoleReport |
C→S | Client reporting NAT hole punch outcome. |
8.2 Server Control Lifecycle (server/control.go)
The Control struct is the core server-side session object, one per connected frpc:
frpc connects
→ newControl() creates Control
→ runReaderWriter() starts message loop
→ handleLogin() validates auth
→ registerControl() adds to ControlManager + ClientRegistry
→ message dispatch loop:
NewProxy → handleNewProxy() → server/proxy/
CloseProxy → handleCloseProxy()
Ping → respond with Pong, update lastPing
NewWorkConn → handleNewWorkConn() → add to work conn pool
NewVisitorConn → handleNewVisitorConn() → server/visitor/
→ heartbeat monitor goroutine watches lastPing vs heartbeatTimeout
frpc disconnects / timeout
→ control.Close() → closes all proxies, removes from registry
8.3 Server Proxy Types (server/proxy/)
Each proxy type implements the Proxy interface. The factory dispatches by proxy_type:
| Type | Implementation | Bind behavior |
|---|---|---|
tcp |
TCPProxy |
Acquires port from port manager, TCP listener |
udp |
UDPProxy |
Acquires port from port manager, UDP listener |
http |
HTTPProxy |
Registers vhost route with HTTP reverse proxy controller |
https |
HTTPSProxy |
Registers vhost route with HTTPS reverse proxy controller (SNI-based) |
stcp |
STCPProxy |
Registers in visitor manager keyed by secret_key |
xtcp |
XTCPProxy |
Registers NAT hole listener; coordinates via nathole package |
sudp |
SUDPProxy |
UDP variant of STCP |
tcpmux |
TCPMuxProxy |
Registers with TCPMux HTTP CONNECT handler |
8.4 Client Connector (client/connector.go)
The connector manages the physical transport connection to frps and reconnection logic:
// Transport selection logic (simplified)
switch config.Transport.Protocol {
case "kcp":
conn = dialKCP(serverAddr, kcpConfig)
case "quic":
conn = dialQUIC(serverAddr, quicConfig)
case "websocket":
conn = dialWebSocket(serverAddr, false)
case "wss":
conn = dialWebSocket(serverAddr, true) // TLS
default: // "tcp"
conn = dialTCP(serverAddr)
}
if config.Transport.TLS.Enable {
conn = tlsWrap(conn, tlsConfig)
}
8.5 NAT Hole Punching (pkg/nathole/)
The XTCP NAT traversal system uses STUN for discovery and a coordination protocol through frps:
- NAT Classification: Each side performs STUN queries to classify NAT type (full cone, restricted cone, symmetric). Classification stored in
nathole/analysis.go. - Coordination: frps acts as signaling server. Visitor sends
NatHoleVisitor; client registers withNatHoleClient. Server sendsNatHoleRespto both with peer address hints. - Hole Punch: Both sides simultaneously send UDP packets to each other's predicted external address. Pion STUN library handles STUN binding requests/responses.
- Session: After hole punch, XTCP uses KCP or QUIC over UDP for the actual tunnel (
client/visitor/xtcp.go). - Fallback: If hole punch fails within
fallbackTimeoutMs, visitor falls back to thefallbackToproxy name (relay via frps). - Analysis data: Outcomes stored for
natholeAnalysisDataReserveHourshours to improve future punch strategies.
8.6 Virtual Network (pkg/vnet/)
Experimental feature gated behind featureGates.VirtualNet = true:
- Creates a TUN network interface using
github.com/songgao/water - Assigns CIDR address from
virtualNet.addressconfig - Uses
github.com/vishvananda/netlinkfor Linux routing table management - WireGuard (
golang.zx2c4.com/wireguard) for encrypted peer-to-peer tunneling - Traffic through XTCP tunnel, enabling full layer-3 virtual network
- Linux only — TUN/netlink are not supported on Windows or macOS
8.7 Config Loading Pipeline (pkg/config/)
1. Raw bytes from file / CLI flags / environment
2. Format detection by file extension (.toml/.yaml/.json/.ini)
3. Template rendering: {{ .Envs.KEY }} expanded
4. Unmarshal into v1 config structs
5. Default value population
6. Validation via pkg/config/v1/validation/
7. Config source aggregation (file + store merge via Aggregator)
The StoreSource in pkg/config/source/ provides dynamic proxy/visitor config that can be modified at runtime via the store API without touching the config file.
8.8 Metrics Architecture (pkg/metrics/)
Three metric backends:
| Backend | Type | Usage |
|---|---|---|
| In-memory | ServerMetrics interface |
Default; powers dashboard API /api/traffic/ |
| Prometheus | prometheus.Metrics |
Enabled via enablePrometheus = true; scrape at /metrics |
| Aggregate | AggregateMetrics |
Wraps both; writes to both simultaneously |
Counters tracked per proxy: connections (current), traffic in bytes, traffic out bytes. Prometheus gauge and counter families follow standard Prometheus naming (frp_server_*).
8.9 Authentication Internals (pkg/auth/)
Verifier (server-side):
- TokenAuthVerifier: constant-time compare of token field
- OIDCAuthVerifier: go-oidc provider, token parsing, audience/issuer checks
- Token cache: avoids redundant OIDC provider fetches
- Retry logic for transient OIDC provider failures
Setter (client-side):
- TokenAuthSetter: injects token into Login/Ping messages
- OIDCAuthSetter: acquires token from configured token source, injects
- Caches token, refreshes before expiry
8.10 E2E Test Framework (test/e2e/)
Tests use Ginkgo v2 + Gomega. Test coverage areas:
| Test Suite | Location | What it tests |
|---|---|---|
| Basic proxying | test/e2e/basic/ |
TCP, UDP, HTTP, HTTPS proxy types end-to-end |
| Plugins | test/e2e/plugin/ |
All 10 client plugins |
| Features | test/e2e/features/ |
Bandwidth limits, heartbeat, load balancing groups, real IP headers, chaos, XTCP, SSH tunnel |
Test helpers spin up real frps/frpc binaries, mock HTTP/stream/OIDC servers, and verify against live connections. Run with:
# Run all e2e tests
go test ./test/e2e/... -v
# Run specific suite
go test ./test/e2e/basic/... -v -ginkgo.focus="TCP"
8.11 Key Interfaces
// Proxy interface (server/proxy/)
type Proxy interface {
Run() (remoteAddr string, err error)
Close()
GetLoginMsg() *msg.Login
GetWorkConnFromPool(ctx context.Context) (net.Conn, error)
RegisterWorkConn(conn net.Conn)
GetUsedPortsNum() int
GetStatus() *ProxyStatus
}
// Auth interfaces (pkg/auth/)
type Verifier interface {
VerifyLogin(m *msg.Login) error
VerifyPing(m *msg.Ping) error
VerifyNewWorkConn(m *msg.NewWorkConn) error
}
type Setter interface {
SetLogin(m *msg.Login) error
SetPing(m *msg.Ping) error
SetNewWorkConn(m *msg.NewWorkConn) error
}
// Config source (pkg/config/source/)
type ConfigSource interface {
GetProxies() ([]*v1.ProxyConfigurer, error)
GetVisitors() ([]*v1.VisitorConfigurer, error)
}
8.12 Dependency Inventory
| Dependency | Purpose |
|---|---|
github.com/spf13/cobra |
CLI framework for both binaries |
github.com/gorilla/mux |
HTTP routing for dashboard/admin APIs |
github.com/gorilla/websocket |
WebSocket transport implementation |
github.com/fatedier/yamux |
TCP stream multiplexing (fork of hashicorp/yamux) |
github.com/quic-go/quic-go |
QUIC transport |
github.com/xtaci/kcp-go/v5 |
KCP UDP transport |
github.com/pion/stun/v3 |
STUN protocol for NAT traversal |
github.com/prometheus/client_golang |
Prometheus metrics |
github.com/coreos/go-oidc/v3 |
OIDC authentication |
github.com/pelletier/go-toml/v2 |
TOML config parsing |
github.com/songgao/water |
TUN device (vnet) |
github.com/vishvananda/netlink |
Linux network routing (vnet) |
golang.org/x/crypto |
SSH protocol |
golang.zx2c4.com/wireguard |
WireGuard encryption (vnet) |
github.com/pion/ice/v2 |
ICE framework for NAT traversal |
k8s.io/apimachinery |
Kubernetes API types (minimal usage in deps) |
k8s.io/client-go |
Kubernetes client (minimal usage in deps) |
9. Integration Points
9.1 Prometheus Integration
# prometheus.yml scrape config
scrape_configs:
- job_name: 'frps'
static_configs:
- targets: ['frps-host:7500']
basic_auth:
username: admin
password: secret
metrics_path: /metrics
9.2 Grafana Dashboard
Key metrics to visualize:
# Active clients
frp_server_client_counts
# Proxy count by type
sum by (type) (frp_server_proxy_counts)
# Inbound traffic rate (bytes/s)
rate(frp_server_traffic_in_bytes_total[1m])
# Connection rate
rate(frp_server_connection_counts[1m])
9.3 Reverse Proxy Integration (nginx)
Put the frps dashboard behind nginx for additional auth and TLS:
server {
listen 443 ssl;
server_name frps-dashboard.example.com;
ssl_certificate /etc/ssl/certs/frps.crt;
ssl_certificate_key /etc/ssl/private/frps.key;
location / {
proxy_pass http://127.0.0.1:7500;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
9.4 SSH Tunnel Integration (OpenSSH compatible)
# Standard SSH reverse tunnel through frp SSH gateway
ssh -p 2222 \
-o StrictHostKeyChecking=no \
-R 8080:localhost:8080 \
[email removed]
# The "user" field before @ is the frp user namespace (any value accepted)
# The -R flag creates a TCP proxy on frps port 8080
9.5 Custom Server Plugin Example
# Minimal Python plugin server
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/frp/handler', methods=['POST'])
def handler():
data = request.json
op = data.get('op')
content = data.get('content', {})
if op == 'Login':
user = content.get('user', '')
if not is_authorized(user):
return jsonify({'reject': True, 'reject_reason': 'Unauthorized'}), 200
if op == 'NewProxy':
proxy_name = content.get('proxy_name', '')
# ACL check for proxy creation
pass
# Allow the operation
return jsonify({}), 200
if __name__ == '__main__':
app.run(port=9001)
9.6 Config Template Variables
# frpc.toml using environment variable substitution
serverAddr = "{{ .Envs.FRP_SERVER_ADDR }}"
serverPort = {{ .Envs.FRP_SERVER_PORT }}
[auth]
token = "{{ .Envs.FRP_AUTH_TOKEN }}"
[[proxies]]
name = "{{ .Envs.HOSTNAME }}-ssh"
type = "tcp"
localPort = 22
remotePort = {{ .Envs.REMOTE_PORT }}
Run with environment variables:
FRP_SERVER_ADDR=frps.example.com \
FRP_AUTH_TOKEN=mysecret \
HOSTNAME=$(hostname) \
REMOTE_PORT=6022 \
frpc -c frpc.toml
Troubleshooting Quick Reference
| Symptom | Likely Cause | Resolution |
|---|---|---|
type [http] not supported |
vhostHTTPPort not set on frps |
Add vhostHTTPPort = 80 to frps config |
port already in use |
Another proxy or service holds the port | Change remotePort or free the port |
port not allowed |
Port outside allowPorts range |
Add port to allowPorts or adjust range |
authentication failed |
Token mismatch | Verify auth.token matches on frps and frpc |
exceed max_ports_per_client |
Client hit maxPortsPerClient limit |
Increase limit or reduce proxy count |
heartbeat timeout |
Network interruption or frps unreachable | Check network; frpc will auto-reconnect |
| TLS handshake error | frps has TLS forced, frpc not configured | Enable TLS on frpc transport.tls config |
| XTCP fails, falls back to relay | NAT type incompatible (both symmetric) | Use STCP relay; XTCP cannot traverse double-symmetric NAT |
| UDP packets truncated | udpPacketSize too small |
Increase udpPacketSize on both sides to 4096+ |
| Dashboard inaccessible | Wrong IP binding | Ensure webServer.addr is reachable; use 0.0.0.0 for remote access |
| Metrics always zero | enablePrometheus not set |
Add enablePrometheus = true to frps config |
| Docker build fails | Go toolchain version mismatch | Match Go version in go.mod (issue #5288) |
Tip
Enable debug logging (
log.level = "debug") temporarily during troubleshooting to see detailed message-level activity. Restore"info"in production to avoid log volume issues.
Tip
Use
frpc verify -c frpc.tomlto validate client config before deploying. This runs the config parser and validator without connecting to frps.