frp — Architecture#

Architectural style#

Client-Server Reverse Proxy Tunnel — Layered with Protocol Abstraction

frp is a purpose-built reverse-proxy tunneling system organized around two symmetrical domains: a server binary (frps) that runs on the public-IP machine, and a client binary (frpc) that runs behind NAT. The architecture is layered:

  1. Transport layer — multi-protocol listeners (TCP/KCP/QUIC/WebSocket/TLS) that all funnel into a single stream-multiplex abstraction (yamux or QUIC streams)
  2. Control plane — a persistent, multiplexed JSON-message channel between frpc and frps (pkg/msg + pkg/transport) that handles login, proxy registration, heartbeat, and work-connection allocation
  3. Data plane — per-proxy “work connections” that carry actual user traffic, established on demand by the control plane
  4. Plugin / extension layer — HTTP webhook plugins (server-side) and visitor p2p tunnels (client-side) that hook into lifecycle events

Evidence: server/service.go wires all transports in NewService; client/control.go wraps a msg.Dispatcher and transport.MessageTransporter for the multiplexed control channel; server/proxy/ and client/proxy/ are parallel sets of per-proxy-type handlers that form the data plane.

Component diagram (textual)#

┌─────────────────────────────────────────────────────────────────┐
│  frps (server)                                                  │
│                                                                 │
│  ┌────────────┐   demux by first-byte   ┌─────────────────┐    │
│  │  mux.Mux   │ ──────────────────────► │ websocket / TLS │    │
│  │ (TCP:7000) │ ─(default)────────────► │  TCP listener   │    │
│  └────────────┘                         └────────┬────────┘    │
│                                                  │             │
│  ┌─────────────┐  ┌────────────┐                 │ Accept      │
│  │ KCP :7000   │  │ QUIC :7000 │                 ▼             │
│  └──────┬──────┘  └─────┬──────┘        ┌─────────────────┐   │
│         │               │               │  HandleListener  │   │
│         └───────────────┘               │  (yamux session) │   │
│                 ▼                       └────────┬────────┘    │
│         handleConnection                         │             │
│              │                                   │             │
│    ┌─────────┴──────────┐              ┌─────────┴─────────┐   │
│    │  msg.Login         │              │  msg.NewWorkConn  │   │
│    │  → RegisterControl │              │  → RegisterWorkConn│  │
│    └─────────┬──────────┘              └─────────┬─────────┘   │
│              │                                   │             │
│    ┌─────────▼──────────┐              ┌─────────▼─────────┐   │
│    │  server.Control    │  request     │  server/proxy/    │   │
│    │  (per frpc client) │ ──────────► │  Handler (per type)│  │
│    │  ControlManager    │              └───────────────────┘   │
│    └─────────┬──────────┘                                      │
│              │ ResourceController                               │
│    ┌─────────▼──────────────────────────────────────┐          │
│    │  ports.Manager │ group.Ctl │ vhost.Routers      │          │
│    │  visitor.Manager │ plugin.Manager               │          │
│    └────────────────────────────────────────────────┘          │
│                                                                 │
│  web dashboard (HTTP)     SSH gateway     NatHole controller    │
└─────────────────────────────────────────────────────────────────┘

                    ▲ TCP/KCP/QUIC control connection (yamux mux)
                    │ JSON-framed msg.* protocol
                    │

┌─────────────────────────────────────────────────────────────────┐
│  frpc (client)                                                  │
│                                                                 │
│  cmd/frpc → client.Service                                      │
│       │                                                         │
│  loopLoginUntilSuccess ──► login() ──► msg.Login/LoginResp      │
│       │                                                         │
│  ┌────▼────────────────────────────────────────────────────┐    │
│  │  client.Control                                         │    │
│  │   msg.Dispatcher  ◄──── control conn (encrypted) ────  │    │
│  │   transport.MessageTransporter                          │    │
│  │        │                                                │    │
│  │  ┌─────▼──────┐   ┌───────────────┐                    │    │
│  │  │proxy.Manager│   │visitor.Manager│                    │    │
│  │  │(per proxy) │   │(STCP/XTCP)    │                    │    │
│  │  └─────┬──────┘   └───────────────┘                    │    │
│  └────────┼──────────────────────────────────────────────-─┘   │
│           │                                                     │
│  Work connection pool  ────► local service (e.g. :22, :80)     │
│                                                                 │
│  admin web server  │  vnet WireGuard controller                 │
└─────────────────────────────────────────────────────────────────┘

Core components#

server.Service#

  • Package: server
  • Responsibility: Top-level server lifecycle. Owns all network listeners (TCP, KCP, QUIC, WebSocket, TLS, SSH gateway), a protocol-sniffing mux (mux.Mux) on the main port, the web dashboard server, the NatHole controller, and all sub-managers. Dispatches every accepted connection to handleConnection after optional yamux stream setup.
  • Key types: Service struct, NewService(*v1.ServerConfig), Run(context.Context)
  • Dependencies: server/controller, server/proxy, server/ports, server/group, server/registry, server/visitor, pkg/auth, pkg/msg, pkg/transport, pkg/util/vhost, pkg/nathole, pkg/ssh, github.com/hashicorp/yamux, github.com/quic-go/quic-go

server.Control / ControlManager#

  • Package: server
  • Responsibility: Per-frpc-client session on the server side. Holds the control connection, drives the msg.Dispatcher for the control protocol, routes NewProxy/CloseProxy/Ping/ReqWorkConn messages, and manages per-proxy server.proxy.Handler instances. ControlManager is an in-memory map of Control objects indexed by runID.
  • Key types: Control, SessionContext, ControlManager
  • Dependencies: server/proxy, pkg/msg, pkg/transport, pkg/auth, pkg/config/v1

client.Service#

  • Package: client
  • Responsibility: Top-level client lifecycle. Drives the reconnect loop (loopLoginUntilSuccess with exponential backoff), manages config hot-reload via source.Aggregator, owns the admin web server and optional WireGuard vnet.Controller.
  • Key types: Service, ServiceOptions (functional-options-style struct), ConnectorCreator func field (enables VirtualClient)
  • Dependencies: client, pkg/auth, pkg/config/v1, pkg/config/source, pkg/vnet

client.Control#

  • Package: client
  • Responsibility: Per-connection client-side control session. Wraps a msg.Dispatcher (over an optionally encrypted conn) and a transport.MessageTransporter (HTTP/2-like lane-keyed request-response on top of the dispatcher). Runs proxy.Manager and visitor.Manager for this session.
  • Key types: Control, SessionContext
  • Dependencies: client/proxy, client/visitor, pkg/msg, pkg/transport, pkg/auth

client.Connector#

  • Package: client
  • Responsibility: Abstracts the underlying physical connection — TCP/TLS/WebSocket, KCP, or QUIC — and (when TCPMux is enabled) produces yamux streams from a single connection. Swappable: ServiceOptions.ConnectorCreator can inject a pipe-based VirtualConnector for in-process testing/VirtualNet.
  • Key types: Connector interface (Open(), Connect(), Close()), defaultConnectorImpl
  • Dependencies: github.com/hashicorp/yamux, github.com/quic-go/quic-go, pkg/transport

pkg/msg#

  • Package: pkg/msg
  • Responsibility: Defines the complete frpc↔frps wire protocol as plain Go structs with JSON tags. A single-byte type tag prefix identifies each message type; msg.ReadMsg/msg.WriteMsg perform length-prefixed framing. Also provides msg.Dispatcher — a goroutine-safe channel-based message dispatcher that demultiplexes incoming messages to registered handlers.
  • Key types: Login, LoginResp, NewProxy, NewProxyResp, NewWorkConn, ReqWorkConn, StartWorkConn, NewVisitorConn, Ping, Pong, UDPPacket, NatHole*, Dispatcher
  • Dependencies: stdlib only

pkg/transport.MessageTransporter#

  • Package: pkg/transport
  • Responsibility: HTTP/2-like multiplexed request-response over the control connection. Callers register a “lane key” and message type, call Do() to send a request and wait for the matching response, or call Dispatch() to route an incoming response to the right waiting goroutine. Used for NewProxy/NewProxyResp and NatHole coordination, where concurrent proxy registrations may overlap on a single connection.
  • Key types: MessageTransporter interface, transporterImpl
  • Dependencies: pkg/msg

server/controller.ResourceController#

  • Package: server/controller
  • Responsibility: Aggregates all server-side resource managers: TCPPortManager, UDPPortManager, VisitorManager, HTTPReverseProxy, VhostHTTPSMuxer, TCPMuxHTTPConnectMuxer, NatHoleController, PluginManager, and the three group controllers (TCP, HTTP, TCPMux). Passed into every server.Control as a dependency bag.
  • Key types: ResourceController struct
  • Dependencies: server/ports, server/visitor, server/group, pkg/util/vhost, pkg/nathole, pkg/plugin/server

client/proxy.Manager + server/proxy.Manager#

  • Package: client/proxy, server/proxy
  • Responsibility (client side): Creates, starts, and hot-reloads proxy handler goroutines for each configured proxy. Each handler opens work connections on demand (via Connector) and forwards data to the local service.
  • Responsibility (server side): Registry for active Handler objects. Each Handler listens on the allocated remote port (or vhost route) and, when a connection arrives, instructs frpc to open a work connection via ReqWorkConn.
  • Key types: Manager, WorkingStatus, Handler interface (server side)
  • Dependencies (client): pkg/msg, pkg/transport, pkg/plugin/client, client/health, pkg/util/net

Data flow#

Typical TCP proxy (e.g., exposing an SSH server behind NAT)#

1. frpc starts → loopLoginUntilSuccess → connector.Open() → TCP connect to frps:7000
   (yamux session established over the TCP conn if TCPMux=true)

2. frpc sends msg.Login over control stream
   frps: handleConnection → RegisterControl → NewControl → ctl.Start()

3. frpc sends msg.NewProxy{ProxyType:"tcp", RemotePort:6000}
   frps: handles NewProxy → server/proxy.Handler created → binds 0.0.0.0:6000

4. Internet user connects to frps:6000
   frps proxy.Handler: sends msg.ReqWorkConn on control channel

5. frpc receives ReqWorkConn → Connector.Connect() → new TCP/yamux stream to frps:7000
   frpc sends msg.NewWorkConn on the new stream

6. frps: RegisterWorkConn → routes stream to waiting proxy.Handler
   frps sends msg.StartWorkConn on the work conn

7. frpc: proxy handler opens conn to local SSH :22
   frpc ↔ frps: raw bidirectional copy between work conn and local conn
   frps ↔ internet user: raw bidirectional copy between work conn and user conn

HTTP vhost proxy flow#

The flow is the same except frps routes to vhost.Routers (a per-hostname/location reverse-proxy router) instead of a bound port. The HTTP reverse proxy in pkg/util/vhost handles Host-header routing to the correct work-connection pool.

NAT hole-punching (XTCP)#

A more complex flow using pkg/nathole (STUN-based): visitor sends NatHoleVisitor to frps, frps forwards NatHoleClient to frpc, both sides use STUN reflection to discover mapped addresses, then attempt direct UDP/TCP hole-punch. If successful, traffic flows peer-to-peer without traversing frps.

Initialization / Bootstrap#

frps bootstrap sequence#

main()
  └── system.EnableCompatibilityMode()   # OS ulimits/RLIMIT_NOFILE
  └── rootCmd.Execute() (Cobra)
        └── runServer(svrCfg)
              ├── log.InitLogger(...)
              ├── server.NewService(cfg)
              │     ├── transport.NewServerTLSConfig(...)
              │     ├── httppkg.NewServer(...)        # dashboard
              │     ├── auth.BuildServerAuth(...)
              │     ├── plugin.Manager + group controllers init
              │     ├── net.Listen("tcp", bindAddr)
              │     ├── mux.NewMux(ln)                # protocol demux
              │     ├── optional KCP, QUIC listeners
              │     ├── ssh.NewGateway(...)
              │     ├── websocket listener from mux
              │     ├── vhost HTTP/HTTPS servers
              │     ├── TLS listener from mux
              │     └── nathole.NewController(...)
              └── svr.Run(ctx)
                    ├── go webServer.Run()
                    ├── go HandleListener(sshTunnelListener)
                    ├── go HandleQUICListener(quicListener)
                    ├── go HandleListener(websocketListener)
                    ├── go HandleListener(tlsListener)
                    ├── go NatHoleController.CleanWorker()
                    ├── go sshTunnelGateway.Run()
                    └── HandleListener(listener)  # blocks; primary TCP

frpc bootstrap sequence#

main()
  └── system.EnableCompatibilityMode()
  └── sub.Execute() (Cobra)
        └── runClient(cfgFile)
              ├── config.LoadClientConfig(...)
              ├── client.NewService(ServiceOptions{...})
              │     ├── auth.BuildClientAuth(...)
              │     ├── aggregator.Load() → proxyCfgs, visitorCfgs
              │     ├── httppkg.NewServer(...)     # admin API
              │     └── vnet.NewController(...)    # if VirtualNet configured
              └── svr.Run(ctx)
                    ├── go webServer.Run()
                    ├── go vnetController.Run()
                    ├── loopLoginUntilSuccess(10s, loginFailExit)
                    │     └── login() → connector.Open → connector.Connect
                    │            → msg.Login/LoginResp handshake
                    │            → NewControl(sessionCtx)
                    │            → ctl.Run(proxyCfgs, visitorCfgs)
                    └── go keepControllerWorking()  # reconnect loop

Dependency injection style: Manual constructor wiring. All dependencies are passed as constructor arguments or via SessionContext structs. No DI framework (no wire, dig, or fx). ServiceOptions acts as a functional-options-like configuration bag that lets callers inject a custom ConnectorCreator (used by VirtualClient for in-process frp tunnels).

Configuration#

  • Format: YAML, TOML, or JSON (v1 format, pkg/config/v1). Legacy ini format is still parsed for backward compatibility (pkg/config/legacy) but deprecated with a printed warning.
  • Loading: config.LoadServerConfig / config.LoadClientConfig detect format from file extension and strict-parse to typed structs.
  • Flags: Cobra persistent flags bound directly to v1.ServerConfig / v1.ClientCommonConfig struct fields via config.RegisterServerConfigFlags. Flags override file values.
  • Client-side hot reload: source.Aggregator aggregates a ConfigSource (file-backed) and optional StoreSource. The client Service watches for changes and calls UpdateAllConfigurerproxy.Manager.UpdateAll, which adds/removes/updates proxy goroutines without restarting the control connection.
  • No Viper: frp uses its own pkg/config loading pipeline built on mapstructure + github.com/spf13/pflag.
  • Environment variables: No direct env-var binding for proxy/server config. The pkg/config/source package has a StoreSource for dynamic config stores (e.g., a sidecar API), not env vars.

Key design decisions#

  1. Single multiplexed control connection (yamux/QUIC) for all signaling. Rather than opening a new TCP connection per proxy or per message, frp uses yamux to multiplex all control streams over one underlying TCP connection. This dramatically reduces connection overhead when a client has dozens of proxies, and avoids NAT mapping exhaustion. The transport.MessageTransporter then layers HTTP/2-style request-response lanes on top of this, enabling concurrent NewProxy/NewProxyResp exchanges on the same stream.

  2. Work connections opened on demand, not pre-allocated. The server sends ReqWorkConn only when a real user connection arrives. The client opens a fresh connection (or yamux stream) for each work request. This avoids idle resource consumption on the client, at the cost of slight per-connection latency. A PoolCount config option allows pre-allocating a small pool of work connections to amortize this cost.

  3. Protocol-sniffing demultiplexer on a single port. mux.Mux inspects the first bytes of incoming connections to route WebSocket, HTTPS/TLS, and plain frp connections through the same bind port. This means frps can serve its entire surface — control plane, HTTP vhost, HTTPS vhost, WebSocket — on a single port, which is friendly to firewalled environments.

  4. pkg/msg as an explicit, versioned protocol boundary. All frpc↔frps messages are defined in a single package as plain structs with JSON tags and single-byte type discriminants. This makes the wire protocol inspectable, testable in isolation, and decoupled from both the client and server domains. Version skew is handled at login (LoginResp.Error if version unsupported).

  5. ConnectorCreator injection enables an in-process VirtualClient. By making the transport creation a first-class parameter (ServiceOptions.ConnectorCreator), frp’s pkg/virtual package can instantiate a fully functional frpc inside the same process as frps, communicating via in-memory pipes. This is used for the VirtualNet WireGuard overlay feature and for programmatic SDK usage, without any changes to the core control/proxy logic.