fatedier/frp
github.com/fatedier/frp ↗ · 516 files · 2.1 MB
frp README | 中文文档 Sponsors frp is an open source project with its ongoing development made possible entirely by the support of our awesome sponsors.
Overview
frp is a fast reverse proxy written in Go that exposes services behind NATs/firewalls via a public server (frps) and an in-NAT client (frpc). It supports TCP, UDP, HTTP, HTTPS, TCPMUX, and secret/P2P modes (stcp, sudp, xtcp) over TCP, KCP, QUIC, WebSocket, or TLS control transports, with token/OIDC auth, HTTP-based server plugins, embedded Vue dashboards, and optional dynamic proxy persistence. The codebase is split into cmd/ entrypoints, server/ and client/ runtimes, and a large pkg/ shared library (config v1, msg protocol, wire v2, auth, nathole, plugins, vnet).
System map
The repo builds two binaries from a single Go module: frps (server, cmd/frps) and frpc (client, cmd/frpc), both driven by cobra. server/service.go constructs a Service that multiplexes control-plane listeners (TCP mux, KCP, QUIC, WebSocket, raw TLS, internal SSH pipe) on shared ports and owns the ControlManager, proxy.Manager, plugin.Manager, ports.Manager and vhost routers. client/service.go constructs a Service that dials frps through a pluggable Connector and keeps a Control session alive with backoff retries. Shared protocol and config code lives under pkg/ (msg, proto/wire, config/v1, auth, transport, util). State is entirely in-memory except an optional client-side JSON store for dynamic proxies. Two Vue 3 apps under web/ provide dashboards and are embedded via the assets package, or excluded with the 'noweb' build tag.
- frps entrypoint is cmd/frps/root.go: cobra rootCmd.RunE loads config via config.LoadServerConfig, validates with validation.NewConfigValidator, then calls runServer -> server.NewService -> svr.Run.
- frpc entrypoint is cmd/frpc/sub/root.go: runClient -> config.LoadClientConfigResult -> source.NewAggregator -> client.NewService -> svr.Run; a --config_dir flag runs one Service per file via runMultipleClients.
- server.Service multiplexes the bind port with golib mux.Mux: default listener for TCP control, a prefix-matched websocket listener ('GET /~frp'), and a TLS listener that sniffs first byte 0x16 / FRPTLSHeadByte.
- Optional listeners are created for KCP (netpkg.ListenKcp on KCPBindPort), QUIC (quic.ListenAddr with ALPN 'frp'), SSH tunnel gateway (ssh.NewGateway), and a tcpmux HTTPConnect multiplexer on TCPMuxHTTPConnectPort.
- The wire protocol has two versions: acceptConnection calls wire.CheckMagic to detect v2, then negotiates crypto/UDP-codec via ClientHello/ServerHello frames before reading the first msg.
- The client dials frps through a Connector interface (client/connector.go) supporting tcp, kcp, quic (one quic.Conn, streams converted via QuicStreamToNetConn), websocket/wss, and tls; ServiceOptions.ConnectorCreator allows swapping in a virtual pipe connector.
- Server state is in-memory: registry.ClientRegistry tracks logical clients by user.clientID, proxy.Manager tracks live proxies, ports.Manager allocates TCP/UDP remote ports honoring AllowPorts, and vhost.Routers route HTTP(S) vhosts.
- The only persistent state is the optional client-side StoreSource: a JSON file (store.path) holding dynamic proxies/visitors, merged with file config by source.Aggregator.
- Dashboards are Vue 3 apps web/frps and web/frpc (npm workspaces under web/), embedded into Go via the assets package; Makefile adds a 'noweb' build tag when web/*/dist is absent.
- Build produces CGO_ENABLED=0 binaries with build tags 'frps'/'frpc' plus optional 'noweb' (Makefile frps/frpc targets); go.mod pins go 1.25.0 with quic-go, kcp-go, yamux, gorilla/mux, go-oidc, prometheus, wireguard as key deps.
- server/http and client/http packages implement admin API controllers behind gorilla/mux routers registered by registerRouteHandlers on each Service's webServer (httppkg.Server).
- Both services disable quic-go receive-buffer warnings and ECN via env vars in package init, and set crypto.DefaultSalt = 'frp' for golib crypto.
Feature catalog
frp exposes eight proxy types (tcp, udp, http, https, tcpmux, stcp, sudp, xtcp) plus three visitor types (stcp, sudp, xtcp) for private/P2P access. Features include token and OIDC auth, per-proxy encryption/compression/bandwidth limits, load-balancer groups, health checks, PROXY protocol and host-header rewriting, HTTP server plugins, client plugins (static_file, unix_domain_socket, http(s) converters, socks5, tls2raw, virtual_net), NAT hole punching via STUN, an SSH tunnel gateway, a WireGuard-based VirtualNet, Prometheus metrics, dashboards for both daemons, and a rich frpc CLI with per-type subcommands, verify, nathole discover and admin commands.
- Eight proxy types are defined by proxyConfigTypeMap in pkg/config/v1/proxy.go (TCP, UDP, HTTP, HTTPS, TCPMUX, STCP, XTCP, SUDP); NewProxyConfigurerByType instantiates typed configurers via reflection.
- frpc auto-generates a cobra subcommand per proxy type (tcp, udp, tcpmux, http, https, stcp, sudp, xtcp) plus visitor subcommands for stcp/sudp/xtcp, wiring flags via config.RegisterClientCommonConfigFlags/RegisterProxyFlags.
- Auth supports token and OIDC methods: NewAuthSetter picks NewTokenAuth or Oidc setters/verifiers, and TokenSource can dynamically resolve tokens (file/exec) via Resolve.
- Client-side plugins registered via init() include static_file, unix_domain_socket, http2http, http2https, https2http, https2https, socks5, tls2raw, virtual_net, and http_proxy; each registers a CreatorFn into pkg/plugin/client's creators map.
- Server manage plugins are HTTP webhooks configured via HTTPPlugins; plugin.Manager supports six ops (Login, NewProxy, CloseProxy, Ping, NewWorkConn, NewUserConn) that can reject or mutate request content.
- xtcp P2P mode uses NAT hole punching: client/visitor exchange NatHoleVisitor/NatHoleClient messages through frps' nathole.Controller, with STUN discovery defaulting to stun.easyvoip.com:3478 (natHoleStunServer).
- stcp/sudp 'secret' proxies require a shared secretKey; visitors bind a local port and connect through frps visitor-manager admission (admitVisitorByRunID / visitor.Manager).
- Load balancing groups exist for TCP (TCPGroupCtl), HTTP (HTTPGroupCtl), HTTPS and TCPMux, letting multiple proxies share a group key and remote port.
- SSH tunnel gateway (SSHTunnelGateway.BindPort) accepts native ssh connections and forwards them into frps via an InternalListener handled like regular frp connections.
- VirtualNet creates a TUN device (water + wireguard-go) so visitors get an IP on a virtual network; enabled via featureGates.VirtualNet and virtualNet.address, and cannot be enabled by hot reload alone.
- frps dashboard API exposes /healthz, /api/serverinfo, /api/proxy/{type}[/{name}], /api/traffic/{name}, /api/clients, DELETE /api/proxies, plus a parallel /api/v2 surface (users, system/info, system/prune, clients, proxies) and /metrics when EnablePrometheus.
- frpc admin API exposes /api/reload, /api/stop, /api/status, GET/PUT /api/config, per-proxy/visitor config reads, and CRUD under /api/store/proxies|visitors when the store source is enabled.
Data flow
Config enters as TOML/YAML/JSON (or deprecated INI), is template-rendered with environment variables, decoded into pkg/config/v1 types, completed, and validated before use. Control-plane data flows as typed msg structs (Login, NewProxy, NewWorkConn, Ping, NatHole*) over a single muxed connection, versioned by the v1/v2 wire protocol. User traffic flows: public listener on frps -> proxy -> pooled work connection -> frpc -> optional plugin -> local service, joined byte streams via libio.Join. Admin data flows through gorilla/mux routers into adminapi controllers, and reloads re-merge config via the source.Aggregator.
- Config loading: LoadFileContentWithTemplate renders Go templates exposing .Envs plus parseNumberRange helpers, then LoadConfigure tries TOML->JSON marshal, JSON detection via yaml.IsJSONBuffer, or YAML (with dot-field stripping), honoring strict mode.
- Legacy INI configs are detected by a 'common' section and converted through pkg/config/legacy converters (Convert_ServerCommonConf_To_v1, Convert_ClientCommonConf_To_v1) before joining the v1 path.
- Client proxy/visitor configs come from inline [[proxies]]/[[visitors]] plus includeConfigFiles globs (LoadAdditionalClientConfigs), dedup-checked by validateNoDuplicateNames, filtered by common.Start and enabled flags in FilterClientConfigurers.
- Login handshake: client sends msg.Login over the control conn; server runs pluginManager.Login hooks, wraps the conn in an encrypted ReadWriter (AEAD for v2, crypto RW for v1), then RegisterControl admits and activates the Control, replying LoginResp{RunID}.
- Proxy registration: client pm.UpdateAll sends msg.NewProxy; server handleNewProxy runs NewProxy plugin hooks, builds a configurer via config.NewProxyConfigurerFromMsg, creates a Proxy through proxyFactoryRegistry, runs it (acquiring ports/vhost routes) and answers NewProxyResp.
- Work connections are pooled: server keeps workConnCh buffered at poolCount+10, sends msg.ReqWorkConn on demand; client handleReqWorkConn dials a fresh conn, writes authenticated NewWorkConn, waits for StartWorkConn, then hands the conn to the owning proxy.
- TCP user traffic path on frpc: BaseProxy.InWorkConn -> HandleTCPWorkConnection -> wrapWorkConn (limiter -> encryption -> compression) -> optional proxyPlugin.Handle or dial localIP:localPort -> libio.Join(localConn, remote).
- HTTP(S) traffic is routed by hostname through vhost.Routers into the matching proxy; frps can share the bind port with vhost ports by muxing HTTP/HTTPS listeners off mux.Mux when BindPort == VhostHTTP(S)Port.
- UDP traffic uses msg.UDPPacket frames; the negotiated codec (udpPacketCodec) is fixed per session via the v2 ServerHello and passed into proxy/visitor managers.
- Hot reload path: /api/reload -> apiController.Reload -> Service.UpdateConfigSource -> configSource.ReplaceAll -> reloadConfigFromSourcesLocked -> aggregator.Load -> Filter/Complete -> UpdateAllConfigurer -> ctl.pm/vm.UpdateAll.
- Metrics flow: server/metrics.Server records NewClient/NewProxy/CloseProxy events during Control lifecycle; pkg/metrics EnableMem/EnablePrometheus are activated when webServer.Port > 0 and exposed at /metrics.
- NatHole orchestration: NatHoleVisitor/NatHoleClient/NatHoleReport messages are dispatched as async handlers on the server Control and routed to nathole.Controller, which analyzes NAT behavior and replies NatHoleResp to visitors.
Behavior traces
Four flows were traced end-to-end. (1) frps startup: cobra -> LoadServerConfig -> validate -> NewService wires all listeners/managers -> Run spawns per-listener goroutines. (2) frpc startup+login: cobra -> LoadClientConfigResult -> aggregator merge -> NewService -> Run -> loopLoginUntilSuccess -> controlSessionDialer.Dial -> NewControl -> ctl.Run. (3) TCP proxy request path: TCPProxy.Run listens -> user conn -> GetWorkConn -> ReqWorkConn -> client NewWorkConn -> StartWorkConn -> InWorkConn -> join to local service. (4) Admin reload: GET /api/reload -> Reload -> UpdateConfigSource -> reloadConfigFromSourcesLocked -> UpdateAllConfigurer.
- frps startup order: rootCmd.RunE -> config.LoadServerConfig (legacy INI detection -> template render -> decode -> Complete) -> security.NewUnsafeFeatures + ValidateServerConfig -> runServer -> log.InitLogger -> server.NewService -> svr.Run.
- server.NewService order: build TLS config -> optional webServer (+metrics) -> BuildServerAuth -> construct managers (ctlManager, pxyManager, pluginManager, ResourceController with port managers) -> register HTTP plugins -> group controllers -> bind muxed TCP listener -> optional KCP/QUIC/SSH/vhost listeners -> NatHoleController.
- svr.Run spawns goroutines: webServer.Run, HandleListener(sshTunnelListener internal=true), HandleListener(kcp), HandleQUICListener, HandleListener(websocket), HandleListener(tls), NatHoleController.CleanWorker, sshTunnelGateway.Run, then blocks in HandleListener(main listener).
- Inbound conn handling: HandleListener -> handleConnection -> acceptConnection (CheckMagic v1/v2, ClientHello negotiation, read first msg) -> switch on msg.Login/msg.NewWorkConn/msg.NewVisitorConn -> RegisterControl / RegisterWorkConn / RegisterVisitorConn.
- Control lifecycle is generation-guarded: ControlManager.Add installs ctl as pending generation keyed by runID (replacing the old control and inheriting its runMu), Activate registers user.clientID in ClientRegistry (conflict -> error), completeLogin writes LoginResp then startLocked flips state to running and launches ctl.worker.
- frpc startup order: runClient -> LoadClientConfigResult -> NewConfigSource.ReplaceAll -> optional NewStoreSource -> NewAggregator.Load -> Filter/Complete configurers -> ValidateAllClientConfig -> startServiceWithAggregator -> client.NewService -> svr.Run.
- client.Service.Run order: optional DNSServer override -> vnetController.Init/Run -> admin webServer -> loopLoginUntilSuccess (first login failure can exit when loginFailExit) -> keepControllerWorking reconnection loop with FastBackoff (3 fast retries/min then exponential to 20s).
- Client login + proxy bring-up: loopLoginUntilSuccess -> controlSessionDialer.Dial(runID) -> NewControl -> ctl.SetInWorkConnCallback -> ctl.Run -> pm.UpdateAll(proxyCfgs) sends NewProxy per proxy and vm.UpdateAll(visitorCfgs); worker starts heartbeatWorker and msgDispatcher.
- Work conn establishment on demand: server GetWorkConn drains pool else sends ReqWorkConn and waits up to UserConnTimeout; client handleReqWorkConn -> connectServer -> authenticated NewWorkConn -> server RegisterWorkConn pushes into workConnCh -> proxy.Start writes StartWorkConn back to client.
- TCP data path completion: client receives StartWorkConn -> pm.HandleWorkConn(proxyName) -> Proxy.InWorkConn -> HandleTCPWorkConnection wraps conn (limit/encrypt/compress) -> writes PROXY protocol header if configured -> dials localIP:localPort -> libio.Join for bidirectional copy.
- Heartbeat: client heartbeatWorker sends authenticated msg.Ping every HeartbeatInterval with backoff and closes session when lastPong exceeds HeartbeatTimeout; server heartbeatWorker closes the Control when lastPing exceeds Transport.HeartbeatTimeout.
- Admin reload trace: GET /api/reload -> apiController.Reload -> Service.UpdateConfigSource (reloadMu serialized) -> cfgSource.ReplaceAll -> reloadConfigFromSourcesLocked -> aggregator.Load -> VirtualNet requirement check -> UpdateAllConfigurer -> ctl.vm.UpdateAll + ctl.pm.UpdateAll.
Testing signals
Unit tests use Go's testing with testify and rodaine/table, spread through pkg/, client/, server/ and cmd/ (run via `make test`/`gotest` with the noweb tag). End-to-end tests use Ginkgo/Gomega under test/e2e with a framework that spawns real frps/frpc binaries, organized into v1/basic, v1/features, plugin, legacy and mock server helpers; `make e2e` runs hack/run-e2e.sh with 16-way parallelism. Cross-version compatibility suites run released binaries via hack/run-e2e-compatibility.sh and download scripts. The web workspace uses Vitest + @vue/test-utils; linting is via golangci-lint (.golangci.yml) plus gofmt/gofumpt/gci/vet make targets and GitHub Actions workflows.
- make gotest runs `go test -tags noweb -v --cover` over ./assets/..., ./cmd/..., ./client/..., ./server/..., ./pkg/...; make alltest adds vet and e2e.
- E2E harness: test/e2e/e2e.go RunE2ETests registers Gomega fail handler, enables EmitSpecProgress and RandomizeAllSpecs; hack/run-e2e.sh installs ginkgo v2.23.4 and runs against bin/frpc and bin/frps (overridable via FRPC_PATH/FRPS_PATH).
- E2E framework provides process management, port allocation, mock servers and request helpers under test/e2e/framework and test/e2e/pkg (cert, plugin, port, process, relay, request, rpc, ssh).
- E2E coverage areas include basic client/server behavior, config, HTTP, tcpmux, xtcp, OIDC, token_source, wire protocol, plus features: bandwidth_limit, chaos, control_replacement, group, heartbeat, monitor, real_ip, ssh_tunnel, store.
- Compatibility testing: make e2e-compatibility runs hack/run-e2e-compatibility.sh against FRP_COMPAT_BASELINE_COUNT released versions (floor 0.61.0); e2e-compatibility-last-frpc/last-frps download the last release into ./lastversion and run the suite with one side pinned old.
- Unit tests exist for config decode/load (pkg/config/v1 *_test.go, pkg/config/load_test.go), msg codecs (pkg/msg msg_test.go, wire_v2_test.go, udp_binary_test.go + benchmarks), auth OIDC (pkg/auth/oidc_test.go), source aggregator/store, client service/config_manager/control_session, server control/service/proxy, nathole analysis/discovery, vnet controller, health.
- cmd/frpc/sub has verify_test.go covering the verify subcommand; most other cmd code (proxy/admin/nathole subcommands) has no direct unit tests.
- Web frontend testing: web/package.json defines test:unit via vitest run (vitest 4.1.10, jsdom, @vue/test-utils) across workspaces shared/frpc/frps; Makefile web-ci runs npm ci, lint:check, test:unit and builds.
- Lint/format toolchain: golangci-lint run configured by .golangci.yml; make fmt (go fmt), fmt-more (gofumpt), gci (import grouping with frp prefix), vet; CI workflows include golangci-lint.yml and goreleaser.yml.
- runMultipleClients (--config_dir) is explicitly documented as 'only designed for testing. It's not guaranteed to be stable' yet is reachable via a public flag.
- Ginkgo parallel runs default to CONCURRENCY=16 in run-e2e.sh and framework.RunID isolates per-node resources.
Risk map
The highest-risk areas are command-execution token sources (gated behind the TokenSourceExec unsafe feature), filesystem-serving and socket-exposing client plugins (static_file, unix_domain_socket which can expose docker.sock), the VirtualNet TUN setup that shells out to ifconfig/route on macOS, and admin web servers that depend on auth middleware correctness. Concurrency risk concentrates in server/control.go's ControlManager, which enforces a documented lock order (runMu -> cm.mu -> lifecycleMu -> registry) and generation handoff barriers; regressions there cause deadlocks or leaked controls. The deprecated INI path (pkg/config/legacy) and the testing-only --config_dir mode are legacy surface. Plugin content mutation uses a deliberate panic on wrong types.
- ExecSource.Resolve executes arbitrary commands (exec.CommandContext with env passthrough) to produce auth tokens; it is gated by the TokenSourceExec entry in both ClientUnsafeFeatures and ServerUnsafeFeatures requiring --allow-unsafe.
- VirtualNet setup on Darwin shells out to ifconfig and route to configure the TUN device; tun_linux.go uses netlink. Errors here are runtime-privilege dependent.
- unix_domain_socket and static_file client plugins expose local filesystem paths/sockets remotely; README documents exposing /var/run/docker.sock as a TCP proxy, an intentionally dangerous capability.
- Admin APIs (client /api/config PUT, /api/stop, /api/store/*; server DELETE /api/proxies, /api/v2/system/prune) are powerful mutation endpoints guarded only by helper.AuthMiddleware on the dashboard port — misconfiguration of webServer auth exposes full control. (inferred)
- ControlManager implements a strict multi-lock protocol (never wait on runMu holding cm.mu; order runMu -> cm.mu -> ctl.lifecycleMu -> registry locks) with generation IDs and handoff barriers; the complexity concentrates deadlock/race risk in control.go.
- GetWorkConn recovers from panics and the worker teardown closes pooled conns, but a full workConnCh silently discards registered work conns ('work connection pool is full, discarding').
- Plugin content mutation in handleMutableContent does retContent.(*T) with a comment 'Buggy Plugin implementations still panic here, by design' — a malicious/broken server plugin path can panic the handler goroutine.
- Server plugins make outbound HTTP calls to user-configured addresses during login/proxy/workconn events; a plugin failure rejects the operation (errors fail closed for mutable ops like Login/NewProxy).
- Legacy INI support is deprecated and routed through pkg/config/legacy converters; conf/legacy holds reference INI files. New features are TOML/YAML/JSON-only, so the legacy path is dead-end code that still runs when a 'common' section is detected.
- xtcp relies on public STUN (default stun.easyvoip.com:3478) and NAT-behavior heuristics in nathole; P2P may silently fail on incompatible NATs — README itself recommends stcp fallback.
- SSH tunnel gateway accepts native SSH connections on a dedicated port and pipes them into the control plane (InternalListener); it expands the server's attack surface to the SSH protocol. (inferred)
- Duplicate proxy/visitor names across merged config sources are rejected at load by validateNoDuplicateNames; before this check a duplicate would have been silently dropped — indicates historically fragile merge semantics. (inferred)
Extension points
frp is built around registries. New proxy types plug in through three coordinated tables: proxyConfigTypeMap in pkg/config/v1, server-side RegisterProxyFactory (init() per type in server/proxy), and client-side RegisterProxyFactory (init() in client/proxy). Client connection-handling plugins register via pkg/plugin/client's Register/Create; server hooks come from external HTTP plugin processes managed by pkg/plugin/server. frpc CLI subcommands are generated automatically from the proxy type list. Config extensibility goes through source.Source implementations aggregated by source.Aggregator, and embedding is supported via pkg/virtual.Client plus ServiceOptions hooks (ConnectorCreator, HandleWorkConnCb). Feature gates (pkg/policy/featuregate) guard experimental features.
- server/proxy uses a reflect.Type-keyed factory registry: each type file (tcp.go, udp.go, http.go, https.go, tcpmux.go, stcp.go, sudp.go, xtcp.go) calls RegisterProxyFactory in init(); adding a server proxy type means adding a config type + factory init.
- client/proxy mirrors the pattern: RegisterProxyFactory keyed by configurer reflect.Type, invoked from NewProxy which first builds a BaseProxy with limiter/encryption key/msgTransporter.
- Client plugins register a CreatorFn by name via plugin.Register (panics on duplicate) and are instantiated lazily in BaseProxy.Run when baseCfg.Plugin.Type is set; the Plugin interface is Handle(ctx, *ConnectionInfo) + Close.
- Server plugin ops are an open enum: Manager.Register bins each Plugin by IsSupport(op) into per-op slices for Login, NewProxy, CloseProxy, Ping, NewWorkConn, NewUserConn; new ops require extending the manager dispatch.
- frpc single-proxy subcommands are generated in a loop over proxyTypes, so a new type automatically gains a CLI command and (for stcp/sudp/xtcp) a nested visitor command.
- Config sources are pluggable: source.Aggregator merges a ConfigSource (in-memory/file) with an optional StoreSource; Service consumes aggregator.Load() at boot and on reload, so new persistence backends slot in as Source implementations.
- Embedding hooks: client.ServiceOptions exposes ConnectorCreator (custom transports/virtual clients) and HandleWorkConnCb (intercept work conns); pkg/virtual.Client demonstrates running frpc against an in-process InternalListener.
- pkg/sdk/client provides a programmatic client API for running proxies from Go code rather than config files.
- Admin routes are registered centrally in each Service's registerRouteHandlers, so new endpoints slot into the gorilla/mux subRouter (with auth middleware already applied); server has a separate encoded-path subrouter for /api/v2 names.
- Feature gates: ClientCommonConfig.FeatureGates map plus pkg/policy/featuregate control rollout of features like VirtualNet; reload path validates gate-dependent config against runtime capabilities.
- vhost.Routers + vhost.NewHTTPReverseProxy/NewHTTPSMuxer are the extension seam for HTTP(S) routing behavior; group controllers (TCP/HTTP/HTTPS/TCPMux) encapsulate shared-port load balancing.
- Message/protocol extension: msg.Dispatcher.RegisterHandler maps message structs to handlers on both sides; adding a control message means defining it in pkg/msg and registering handlers in client/control.go and server/control.go.
Contributor handoff
Start with AGENTS.md for the command surface: `make build|frps|frpc|test|e2e|fmt|fmt-more|gci|vet`, `golangcici-lint run`, `make web`. Read cmd/frps/root.go + cmd/frpc/sub/root.go, then server/service.go and client/service.go for wiring, and server/control.go for the lifecycle/locking protocol before touching session code. Config changes must go through pkg/config/v1 types + validation and be tested for TOML/YAML/JSON and strict mode. Protocol changes must update pkg/msg on both sides and consider the v2 wire negotiation in pkg/proto/wire. E2E tests need built binaries and Ginkgo; unit tests run with the noweb tag when dashboards aren't built.
- AGENTS.md documents the canonical commands: make build/frps/frpc/test/e2e/e2e-trace/alltest/fmt/fmt-more/gci/vet, make web for dashboards, and points to doc/agents/ runbooks (release.md).
- First files to read: cmd/frps/root.go and cmd/frpc/sub/root.go (entrypoints), server/service.go and client/service.go (composition roots), server/control.go (session lifecycle/locking), pkg/config/v1 (config schema), pkg/msg/msg.go (protocol).
- Build tags matter: 'frps'/'frpc' select per-binary code and 'noweb' is auto-added when web/*/dist is missing — run go test/vet with -tags noweb or build dashboards first.
- When editing server/control.go, respect the documented global lock order (runMu, cm.mu, ctl.lifecycleMu, then registry locks) and the pending/activated/running state machine — generation handoff uses barriers between replaced controls.
- Config edits require updating v1 types, Complete() defaults, validation.Validate* and ideally e2e coverage; strict_config=true by default rejects unknown fields, so new fields break older parsers cross-version.
- Client config now flows through source.Aggregator (ConfigSource + StoreSource); bypassing it breaks /api/reload and store CRUD. Keep Service.reloadMu semantics when touching UpdateConfigSource.
- Run `go test -tags noweb ./pkg/... ./client/... ./server/...` for unit tests; `make e2e` requires bin/frps+bin/frpc built first and ginkgo installed (script auto-installs).
- Don't add proxy types piecemeal: a type needs a configurer in proxyConfigTypeMap, marshal/unmarshal to msg, server factory init, client factory init, validation, and usually e2e — the CLI subcommand is then auto-generated.
- Auth token bytes double as the work-conn encryption key (ClientAuth/ServerAuth key = []byte(resolved.Token)) — changing token handling affects transport encryption.
- Version pinning/compat is enforced by tests: FRP_COMPAT_FLOOR_VERSION=0.61.0 and baseline-count e2e runs mean wire/protocol changes must remain compatible across ~8 released versions.
- Logging uses pkg/util/log and xlog (context-scoped, prefixed by runID); follow existing xl.Debugf/Tracef patterns rather than fmt.Println in service code (entrypoints print user-facing errors via fmt).
- Panic-on-duplicate registries (plugin.Register, proxy factories) run in init() — import cycles or double registration crash at startup, so new plugins/types must use unique names/types.
Languages
| Language | Files | Share of source |
|---|---|---|
| Go | 351 | 77% |
| Vue | 47 | 10% |
| TypeScript | 42 | 9% |
| Markdown | 14 | docs/config |
| YAML | 11 | docs/config |
| JSON | 10 | docs/config |
| CSS | 4 | docs/config |
| Sass | 4 | 1% |
Start reading here
Probable source entrypoint: cmd/frpc — inferred from the directory tree and manifest, not the upstream docs.
Source roots
pkg/ (189 files)
Repository structure
| Top-level path | Files |
|---|---|
pkg/ | 189 |
web/ | 128 |
test/ | 70 |
server/ | 44 |
client/ | 34 |
cmd/ | 10 |
.github/ | 9 |
conf/ | 6 |
Docs found: AGENTS.mdCLAUDE.mdREADME.md
Package
go.mod — github.com/fatedier/frp, 0 declared dependencies.
CI & config entrypoints
README.md— Repository readmeAGENTS.md— Agent instructionsCLAUDE.md— Claude instructions.github/workflows/build-and-push-image.yml— GitHub Actions workflow.github/workflows/goreleaser.yml— GitHub Actions workflow.github/workflows/stale.yml— GitHub Actions workflow.github/workflows/golangci-lint.yml— GitHub Actions workflowweb/package.json— npm scripts: test:unit, test:unit:watchweb/frpc/package.json— npm scripts: build, build-only, dev, lint, lint:check, preview, type-checkweb/frps/package.json— npm scripts: build, build-only, dev, lint, lint:check, preview, type-check
Structural health signals
Average score 7.5/10 across 96 analyzed source files; 12 hotspots surfaced.
test/e2e/v1/features/group.go— 598 lines, score 5.7 (Large file, Long function/block)server/http/controller_v2_test.go— 908 lines, score 5.9 (Large file, High churn)web/frps/src/views/ClientDetail.vue— 632 lines, score 5.9 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
d20a232996002026-09-10 — test: strengthen health check coverage (#5528)b0d840858fed2026-09-10 — ci: upgrade CircleCI config to 2.1 (#5530)c5508627c4c42026-09-10 — docs: add rapidproxy as gold sponsor (#5529)832df8dff66d2026-08-29 — fix(health): reset failed count after successful check (#5502)ebb4773c2cd42026-08-29 — docs: update release notes (#5521)